From d935cb0d2f38ecdc3bf79f1b423b6ed33d66f2e5 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 7 Jun 2023 22:34:57 -0400 Subject: [PATCH 01/53] [shared_preferences] Fix initialization race (#4159) During the NNBD transition, the structure of the completer logic in the initialization flow was incorrectly changed to set the field after an `await` instead of immediately. Also updates the error handling to handle `Error` the same way it currently handles `Exception`, which this change surfaced. Fixes https://github.com/flutter/flutter/issues/42407 --- .../shared_preferences/CHANGELOG.md | 4 +- .../lib/shared_preferences.dart | 4 +- .../shared_preferences/pubspec.yaml | 2 +- .../test/shared_preferences_test.dart | 358 +++++++++--------- 4 files changed, 193 insertions(+), 175 deletions(-) diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 3ee0cabefd7a..af254c9aa780 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT +## 2.1.2 +* Fixes singleton initialization race condition introduced during NNBD + transition. * Updates minimum supported macOS version to 10.14. * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index b7690e06756f..c116c9d4c3cc 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -62,11 +62,12 @@ class SharedPreferences { if (_completer == null) { final Completer completer = Completer(); + _completer = completer; try { final Map preferencesMap = await _getSharedPreferencesMap(); completer.complete(SharedPreferences._(preferencesMap)); - } on Exception catch (e) { + } catch (e) { // If there's an error, explicitly return the future with an error. // then set the completer to null so we can retry. completer.completeError(e); @@ -74,7 +75,6 @@ class SharedPreferences { _completer = null; return sharedPrefsFuture; } - _completer = completer; } return _completer!.future; } diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index 02335fe2e7aa..2771b99eea61 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. repository: https://github.com/flutter/packages/tree/main/packages/shared_preferences/shared_preferences issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.1.1 +version: 2.1.2 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart index 06c853dce9b9..805d5ed61110 100755 --- a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart @@ -10,199 +10,196 @@ import 'package:shared_preferences_platform_interface/shared_preferences_platfor void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group('SharedPreferences', () { - const String testString = 'hello world'; - const bool testBool = true; - const int testInt = 42; - const double testDouble = 3.14159; - const List testList = ['foo', 'bar']; - const Map testValues = { - 'flutter.String': testString, - 'flutter.bool': testBool, - 'flutter.int': testInt, - 'flutter.double': testDouble, - 'flutter.List': testList, - }; - - const String testString2 = 'goodbye world'; - const bool testBool2 = false; - const int testInt2 = 1337; - const double testDouble2 = 2.71828; - const List testList2 = ['baz', 'quox']; - const Map testValues2 = { - 'flutter.String': testString2, - 'flutter.bool': testBool2, - 'flutter.int': testInt2, - 'flutter.double': testDouble2, - 'flutter.List': testList2, - }; - - late FakeSharedPreferencesStore store; - late SharedPreferences preferences; - - setUp(() async { - store = FakeSharedPreferencesStore(testValues); - SharedPreferencesStorePlatform.instance = store; - preferences = await SharedPreferences.getInstance(); - store.log.clear(); - }); + const String testString = 'hello world'; + const bool testBool = true; + const int testInt = 42; + const double testDouble = 3.14159; + const List testList = ['foo', 'bar']; + const Map testValues = { + 'flutter.String': testString, + 'flutter.bool': testBool, + 'flutter.int': testInt, + 'flutter.double': testDouble, + 'flutter.List': testList, + }; + + const String testString2 = 'goodbye world'; + const bool testBool2 = false; + const int testInt2 = 1337; + const double testDouble2 = 2.71828; + const List testList2 = ['baz', 'quox']; + const Map testValues2 = { + 'flutter.String': testString2, + 'flutter.bool': testBool2, + 'flutter.int': testInt2, + 'flutter.double': testDouble2, + 'flutter.List': testList2, + }; + + late FakeSharedPreferencesStore store; + late SharedPreferences preferences; + + setUp(() async { + store = FakeSharedPreferencesStore(testValues); + SharedPreferencesStorePlatform.instance = store; + preferences = await SharedPreferences.getInstance(); + store.log.clear(); + }); - test('reading', () async { - expect(preferences.get('String'), testString); - expect(preferences.get('bool'), testBool); - expect(preferences.get('int'), testInt); - expect(preferences.get('double'), testDouble); - expect(preferences.get('List'), testList); - expect(preferences.getString('String'), testString); - expect(preferences.getBool('bool'), testBool); - expect(preferences.getInt('int'), testInt); - expect(preferences.getDouble('double'), testDouble); - expect(preferences.getStringList('List'), testList); - expect(store.log, []); - }); + test('reading', () async { + expect(preferences.get('String'), testString); + expect(preferences.get('bool'), testBool); + expect(preferences.get('int'), testInt); + expect(preferences.get('double'), testDouble); + expect(preferences.get('List'), testList); + expect(preferences.getString('String'), testString); + expect(preferences.getBool('bool'), testBool); + expect(preferences.getInt('int'), testInt); + expect(preferences.getDouble('double'), testDouble); + expect(preferences.getStringList('List'), testList); + expect(store.log, []); + }); - test('writing', () async { - await Future.wait(>[ - preferences.setString('String', testString2), - preferences.setBool('bool', testBool2), - preferences.setInt('int', testInt2), - preferences.setDouble('double', testDouble2), - preferences.setStringList('List', testList2) - ]); - expect( + test('writing', () async { + await Future.wait(>[ + preferences.setString('String', testString2), + preferences.setBool('bool', testBool2), + preferences.setInt('int', testInt2), + preferences.setDouble('double', testDouble2), + preferences.setStringList('List', testList2) + ]); + expect( + store.log, + [ + isMethodCall('setValue', arguments: [ + 'String', + 'flutter.String', + testString2, + ]), + isMethodCall('setValue', arguments: [ + 'Bool', + 'flutter.bool', + testBool2, + ]), + isMethodCall('setValue', arguments: [ + 'Int', + 'flutter.int', + testInt2, + ]), + isMethodCall('setValue', arguments: [ + 'Double', + 'flutter.double', + testDouble2, + ]), + isMethodCall('setValue', arguments: [ + 'StringList', + 'flutter.List', + testList2, + ]), + ], + ); + store.log.clear(); + + expect(preferences.getString('String'), testString2); + expect(preferences.getBool('bool'), testBool2); + expect(preferences.getInt('int'), testInt2); + expect(preferences.getDouble('double'), testDouble2); + expect(preferences.getStringList('List'), testList2); + expect(store.log, equals([])); + }); + + test('removing', () async { + const String key = 'testKey'; + await preferences.remove(key); + expect( store.log, - [ - isMethodCall('setValue', arguments: [ - 'String', - 'flutter.String', - testString2, - ]), - isMethodCall('setValue', arguments: [ - 'Bool', - 'flutter.bool', - testBool2, - ]), - isMethodCall('setValue', arguments: [ - 'Int', - 'flutter.int', - testInt2, - ]), - isMethodCall('setValue', arguments: [ - 'Double', - 'flutter.double', - testDouble2, - ]), - isMethodCall('setValue', arguments: [ - 'StringList', - 'flutter.List', - testList2, - ]), - ], - ); - store.log.clear(); - - expect(preferences.getString('String'), testString2); - expect(preferences.getBool('bool'), testBool2); - expect(preferences.getInt('int'), testInt2); - expect(preferences.getDouble('double'), testDouble2); - expect(preferences.getStringList('List'), testList2); - expect(store.log, equals([])); - }); + List.filled( + 1, + isMethodCall( + 'remove', + arguments: 'flutter.$key', + ), + growable: true, + )); + }); - test('removing', () async { - const String key = 'testKey'; - await preferences.remove(key); - expect( - store.log, - List.filled( - 1, - isMethodCall( - 'remove', - arguments: 'flutter.$key', - ), - growable: true, - )); - }); + test('containsKey', () async { + const String key = 'testKey'; - test('containsKey', () async { - const String key = 'testKey'; + expect(false, preferences.containsKey(key)); - expect(false, preferences.containsKey(key)); + await preferences.setString(key, 'test'); + expect(true, preferences.containsKey(key)); + }); - await preferences.setString(key, 'test'); - expect(true, preferences.containsKey(key)); - }); + test('clearing', () async { + await preferences.clear(); + expect(preferences.getString('String'), null); + expect(preferences.getBool('bool'), null); + expect(preferences.getInt('int'), null); + expect(preferences.getDouble('double'), null); + expect(preferences.getStringList('List'), null); + expect(store.log, [isMethodCall('clear', arguments: null)]); + }); - test('clearing', () async { - await preferences.clear(); - expect(preferences.getString('String'), null); - expect(preferences.getBool('bool'), null); - expect(preferences.getInt('int'), null); - expect(preferences.getDouble('double'), null); - expect(preferences.getStringList('List'), null); - expect(store.log, [isMethodCall('clear', arguments: null)]); - }); + test('reloading', () async { + await preferences.setString('String', testString); + expect(preferences.getString('String'), testString); - test('reloading', () async { - await preferences.setString('String', testString); - expect(preferences.getString('String'), testString); + SharedPreferences.setMockInitialValues(testValues2.cast()); + expect(preferences.getString('String'), testString); - SharedPreferences.setMockInitialValues( - testValues2.cast()); - expect(preferences.getString('String'), testString); + await preferences.reload(); + expect(preferences.getString('String'), testString2); + }); - await preferences.reload(); - expect(preferences.getString('String'), testString2); - }); + test('back to back calls should return same instance.', () async { + final Future first = SharedPreferences.getInstance(); + final Future second = SharedPreferences.getInstance(); + expect(await first, await second); + }); - test('back to back calls should return same instance.', () async { - final Future first = SharedPreferences.getInstance(); - final Future second = SharedPreferences.getInstance(); - expect(await first, await second); + test('string list type is dynamic (usually from method channel)', () async { + SharedPreferences.setMockInitialValues({ + 'dynamic_list': ['1', '2'] }); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final List? value = prefs.getStringList('dynamic_list'); + expect(value, ['1', '2']); + }); + + group('mocking', () { + const String key = 'dummy'; + const String prefixedKey = 'flutter.$key'; - test('string list type is dynamic (usually from method channel)', () async { - SharedPreferences.setMockInitialValues({ - 'dynamic_list': ['1', '2'] - }); + test('test 1', () async { + SharedPreferences.setMockInitialValues( + {prefixedKey: 'my string'}); final SharedPreferences prefs = await SharedPreferences.getInstance(); - final List? value = prefs.getStringList('dynamic_list'); - expect(value, ['1', '2']); + final String? value = prefs.getString(key); + expect(value, 'my string'); }); - group('mocking', () { - const String key = 'dummy'; - const String prefixedKey = 'flutter.$key'; - - test('test 1', () async { - SharedPreferences.setMockInitialValues( - {prefixedKey: 'my string'}); - final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String? value = prefs.getString(key); - expect(value, 'my string'); - }); - - test('test 2', () async { - SharedPreferences.setMockInitialValues( - {prefixedKey: 'my other string'}); - final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String? value = prefs.getString(key); - expect(value, 'my other string'); - }); + test('test 2', () async { + SharedPreferences.setMockInitialValues( + {prefixedKey: 'my other string'}); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final String? value = prefs.getString(key); + expect(value, 'my other string'); }); + }); - test('writing copy of strings list', () async { - final List myList = []; - await preferences.setStringList('myList', myList); - myList.add('foobar'); + test('writing copy of strings list', () async { + final List myList = []; + await preferences.setStringList('myList', myList); + myList.add('foobar'); - final List cachedList = preferences.getStringList('myList')!; - expect(cachedList, []); + final List cachedList = preferences.getStringList('myList')!; + expect(cachedList, []); - cachedList.add('foobar2'); + cachedList.add('foobar2'); - expect(preferences.getStringList('myList'), []); - }); + expect(preferences.getStringList('myList'), []); }); test('calling mock initial values with non-prefixed keys succeeds', () async { @@ -214,6 +211,16 @@ void main() { expect(value, 'foo'); }); + test('getInstance always returns the same instance', () async { + SharedPreferencesStorePlatform.instance = SlowInitSharedPreferencesStore(); + + final Future firstFuture = + SharedPreferences.getInstance(); + final Future secondFuture = + SharedPreferences.getInstance(); + expect(identical(await firstFuture, await secondFuture), true); + }); + test('calling setPrefix after getInstance throws', () async { const String newPrefix = 'newPrefix'; @@ -367,6 +374,15 @@ class UnimplementedSharedPreferencesStore } } +class SlowInitSharedPreferencesStore + extends UnimplementedSharedPreferencesStore { + @override + Future> getAll() async { + await Future.delayed(const Duration(seconds: 1)); + return {}; + } +} + class ThrowingSharedPreferencesStore extends SharedPreferencesStorePlatform { @override Future clear() { From 1057ab48dbfaa085e4ebe75a3b321110f26b81ab Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Thu, 8 Jun 2023 07:15:12 -0700 Subject: [PATCH 02/53] [in_app_purchase] Make the _FeatureCard constructor const in the Android example app (#4162) This works around a Dart analyzer error that is blocking the engine->framework roll (see https://github.com/flutter/flutter/pull/128476) --- .../in_app_purchase_android/example/lib/main.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart index 39ae8f75ca23..86cd8d8841bb 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart @@ -145,7 +145,7 @@ class _MyAppState extends State<_MyApp> { _buildConnectionCheckTile(), _buildProductList(), _buildConsumableBox(), - _FeatureCard(), + const _FeatureCard(), ], ), ); @@ -444,9 +444,9 @@ class _MyAppState extends State<_MyApp> { } class _FeatureCard extends StatelessWidget { - _FeatureCard(); + const _FeatureCard(); - final InAppPurchaseAndroidPlatformAddition addition = + InAppPurchaseAndroidPlatformAddition get addition => InAppPurchasePlatformAddition.instance! as InAppPurchaseAndroidPlatformAddition; From e13b8c43386a46a366c9fd5cd16dea59c64c89aa Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 8 Jun 2023 10:38:03 -0400 Subject: [PATCH 03/53] [tool] Only run unit tests in Chrome for inline web (#4153) Currently we are running Dart unit tests in Chrome for any plugin with web support, but it should only be necessary for plugins that have an inline web implementation, not for app-facing packages that endorse a web implementation. --- .../video_player/test/video_player_test.dart | 4 ++-- .../windows_unit_tests_exceptions.yaml | 8 ------- script/tool/lib/src/test_command.dart | 4 +++- script/tool/test/test_command_test.dart | 23 ++++++++++++++++++- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index ef4f0bfa2104..bef0298c80c1 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -350,12 +350,12 @@ void main() { test('file with special characters', () async { final VideoPlayerController controller = - VideoPlayerController.file(File('A #1 Hit?.avi')); + VideoPlayerController.file(File('A #1 Hit.avi')); await controller.initialize(); final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!; expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri'); - expect(uri.endsWith('/A%20%231%20Hit%3F.avi'), true, + expect(uri.endsWith('/A%20%231%20Hit.avi'), true, reason: 'Actual string: $uri'); }, skip: kIsWeb /* Web does not support file assets. */); diff --git a/script/configs/windows_unit_tests_exceptions.yaml b/script/configs/windows_unit_tests_exceptions.yaml index b837d567713a..76396080fb7d 100644 --- a/script/configs/windows_unit_tests_exceptions.yaml +++ b/script/configs/windows_unit_tests_exceptions.yaml @@ -11,20 +11,12 @@ # Unit tests for plugins that support web currently run in # Chrome, which isn't currently supported by web infrastructure. # TODO(ditman): Fix this in the repo tooling. -- camera - camera_web -- file_selector - file_selector_web -- google_maps_flutter - google_maps_flutter_web -- google_sign_in - google_sign_in_web -- image_picker - image_picker_for_web -- shared_preferences - shared_preferences_web -- url_launcher - url_launcher_web -- video_player - video_player_web - webview_flutter_web diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart index 5101b8f19e7e..5c793f63ed4b 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/test_command.dart @@ -66,7 +66,9 @@ class TestCommand extends PackageLoopingCommand { '--color', if (experiment.isNotEmpty) '--enable-experiment=$experiment', // TODO(ditman): Remove this once all plugins are migrated to 'drive'. - if (pluginSupportsPlatform(platformWeb, package)) '--platform=chrome', + if (pluginSupportsPlatform(platformWeb, package, + requiredMode: PlatformSupport.inline)) + '--platform=chrome', ], workingDir: package.directory, ); diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart index 5d34e390fd00..c4c655bf719f 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/test_command_test.dart @@ -15,7 +15,7 @@ import 'mocks.dart'; import 'util.dart'; void main() { - group('$TestCommand', () { + group('TestCommand', () { late FileSystem fileSystem; late Platform mockPlatform; late Directory packagesDir; @@ -239,6 +239,27 @@ void main() { ); }); + test('Does not run on Chrome for web endorsements', () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: ['test/empty_test.dart'], + platformSupport: { + platformWeb: const PlatformDetails(PlatformSupport.federated), + }, + ); + + await runCapturingPrint(runner, ['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], plugin.path), + ]), + ); + }); + test('enable-experiment flag', () async { final RepositoryPackage plugin = createFakePlugin('a', packagesDir, extraFiles: ['test/empty_test.dart']); From 3e0a170706db5cab0a48c0caf11860fe69a03d8c Mon Sep 17 00:00:00 2001 From: Barnabas A Nsoh Date: Thu, 8 Jun 2023 15:45:55 +0000 Subject: [PATCH 04/53] Fix stale ignore: prefer_const_constructors from flutter/packages (#4154) Update the flutter version in affected packages to 3.10.0 Fix stale ignore: prefer_const_constructors from flutter/packages Fixes https://github.com/flutter/flutter/issues/128141 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* --- packages/flutter_adaptive_scaffold/CHANGELOG.md | 5 +++++ .../example/lib/main.dart | 16 +++++----------- .../example/pubspec.yaml | 4 ++-- packages/flutter_adaptive_scaffold/pubspec.yaml | 4 ++-- packages/flutter_markdown/CHANGELOG.md | 5 +++++ packages/flutter_markdown/pubspec.yaml | 4 ++-- packages/flutter_markdown/test/list_test.dart | 12 ++++-------- .../test/markdown_body_shrink_wrap_test.dart | 12 ++++-------- .../google_maps_flutter/CHANGELOG.md | 5 +++++ .../google_maps_flutter/example/pubspec.yaml | 4 ++-- .../google_maps_flutter/pubspec.yaml | 4 ++-- .../test/map_creation_test.dart | 8 ++------ packages/local_auth/local_auth/CHANGELOG.md | 3 ++- .../local_auth/local_auth/example/lib/main.dart | 12 ++++-------- .../local_auth/local_auth/example/pubspec.yaml | 4 ++-- packages/local_auth/local_auth/pubspec.yaml | 4 ++-- .../local_auth/local_auth_android/CHANGELOG.md | 5 +++++ .../local_auth_android/example/lib/main.dart | 12 ++++-------- .../local_auth_android/example/pubspec.yaml | 4 ++-- .../local_auth/local_auth_android/pubspec.yaml | 4 ++-- packages/local_auth/local_auth_ios/CHANGELOG.md | 5 +++++ .../local_auth_ios/example/lib/main.dart | 12 ++++-------- .../local_auth_ios/example/pubspec.yaml | 4 ++-- packages/local_auth/local_auth_ios/pubspec.yaml | 4 ++-- .../local_auth/local_auth_windows/CHANGELOG.md | 3 ++- .../local_auth_windows/example/lib/main.dart | 12 ++++-------- .../local_auth_windows/example/pubspec.yaml | 4 ++-- .../local_auth/local_auth_windows/pubspec.yaml | 4 ++-- packages/rfw/CHANGELOG.md | 5 +++++ packages/rfw/example/remote/lib/main.dart | 14 +++----------- packages/rfw/example/remote/pubspec.yaml | 4 ++-- packages/rfw/pubspec.yaml | 4 ++-- packages/url_launcher/url_launcher/CHANGELOG.md | 2 ++ .../url_launcher/example/lib/encoding.dart | 10 ++-------- .../url_launcher/example/pubspec.yaml | 4 ++-- packages/url_launcher/url_launcher/pubspec.yaml | 4 ++-- 36 files changed, 106 insertions(+), 120 deletions(-) diff --git a/packages/flutter_adaptive_scaffold/CHANGELOG.md b/packages/flutter_adaptive_scaffold/CHANGELOG.md index 6ff69f593d36..0c907565f1ce 100644 --- a/packages/flutter_adaptive_scaffold/CHANGELOG.md +++ b/packages/flutter_adaptive_scaffold/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + ## 0.1.4 * Use Material 3 NavigationBar instead of BottomNavigationBar diff --git a/packages/flutter_adaptive_scaffold/example/lib/main.dart b/packages/flutter_adaptive_scaffold/example/lib/main.dart index 994b9c8b835c..5e97f06859a9 100644 --- a/packages/flutter_adaptive_scaffold/example/lib/main.dart +++ b/packages/flutter_adaptive_scaffold/example/lib/main.dart @@ -412,11 +412,9 @@ class _LargeComposeIcon extends StatelessWidget { child: Column(children: [ Container( padding: const EdgeInsets.fromLTRB(6, 0, 0, 0), - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: const [ + children: [ Text( 'REPLY', style: TextStyle(color: Colors.deepPurple, fontSize: 15), @@ -444,14 +442,10 @@ class _LargeComposeIcon extends StatelessWidget { ), width: 200, height: 50, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Padding( - padding: const EdgeInsets.fromLTRB(16.0, 0, 0, 0), - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors + child: const Padding( + padding: EdgeInsets.fromLTRB(16.0, 0, 0, 0), child: Row( - children: const [ + children: [ Icon(Icons.edit_outlined), SizedBox(width: 20), Center(child: Text('Compose')), diff --git a/packages/flutter_adaptive_scaffold/example/pubspec.yaml b/packages/flutter_adaptive_scaffold/example/pubspec.yaml index 70ca3041abe6..54e91a84f662 100644 --- a/packages/flutter_adaptive_scaffold/example/pubspec.yaml +++ b/packages/flutter_adaptive_scaffold/example/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' version: 0.0.1 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/flutter_adaptive_scaffold/pubspec.yaml b/packages/flutter_adaptive_scaffold/pubspec.yaml index d988420a2221..4e3cc5c57d42 100644 --- a/packages/flutter_adaptive_scaffold/pubspec.yaml +++ b/packages/flutter_adaptive_scaffold/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ repository: https://github.com/flutter/packages/tree/main/packages/flutter_adaptive_scaffold environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/flutter_markdown/CHANGELOG.md b/packages/flutter_markdown/CHANGELOG.md index 3e6a59396a2e..21a7e338e1ea 100644 --- a/packages/flutter_markdown/CHANGELOG.md +++ b/packages/flutter_markdown/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + ## 0.6.15 * Fixes unawaited_futures violations. diff --git a/packages/flutter_markdown/pubspec.yaml b/packages/flutter_markdown/pubspec.yaml index 74b886c1c75d..525222b9c282 100644 --- a/packages/flutter_markdown/pubspec.yaml +++ b/packages/flutter_markdown/pubspec.yaml @@ -7,8 +7,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 0.6.15 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/flutter_markdown/test/list_test.dart b/packages/flutter_markdown/test/list_test.dart index 0703dfd44483..4a587c50a93f 100644 --- a/packages/flutter_markdown/test/list_test.dart +++ b/packages/flutter_markdown/test/list_test.dart @@ -194,10 +194,8 @@ void defineTests() { await tester.pumpWidget( boilerplate( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - Column( - children: const [ + const Column( + children: [ MarkdownBody(fitContent: false, data: data), ], ), @@ -219,10 +217,8 @@ void defineTests() { await tester.pumpWidget( boilerplate( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - Column( - children: const [ + const Column( + children: [ MarkdownBody(data: data), ], ), diff --git a/packages/flutter_markdown/test/markdown_body_shrink_wrap_test.dart b/packages/flutter_markdown/test/markdown_body_shrink_wrap_test.dart index 9dc611be70a6..a5ae86953921 100644 --- a/packages/flutter_markdown/test/markdown_body_shrink_wrap_test.dart +++ b/packages/flutter_markdown/test/markdown_body_shrink_wrap_test.dart @@ -16,10 +16,8 @@ void defineTests() { 'Then it wraps its content', (WidgetTester tester) async { await tester.pumpWidget(boilerplate( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - Stack( - children: const [ + const Stack( + children: [ Text('shrinkWrap=true'), Align( alignment: Alignment.bottomCenter, @@ -48,10 +46,8 @@ void defineTests() { 'Then it expands to the maximum allowed height', (WidgetTester tester) async { await tester.pumpWidget(boilerplate( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - Stack( - children: const [ + const Stack( + children: [ Text('shrinkWrap=false test'), Align( alignment: Alignment.bottomCenter, diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index a0b461f10eb1..e244734a438e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + ## 2.3.0 * Endorses [`google_maps_flutter_web`](https://pub.dev/packages/google_maps_flutter_web) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index 9c0ab999ddbc..6b3d16007c4b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the google_maps_flutter plugin. publish_to: none environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: cupertino_icons: ^1.0.5 diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index d1e577027b37..91bc476fda86 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.3.0 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" flutter: plugin: diff --git a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart index 10e118486a31..7f88b60ad6c7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart @@ -26,14 +26,10 @@ void main() { ) async { // Inject two map widgets... await tester.pumpWidget( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - Directionality( + const Directionality( textDirection: TextDirection.ltr, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors child: Column( - children: const [ + children: [ GoogleMap( initialCameraPosition: CameraPosition( target: LatLng(43.362, -5.849), diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index b542eda207af..fb370ef1afb0 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -1,7 +1,8 @@ ## NEXT -* Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. ## 2.1.6 diff --git a/packages/local_auth/local_auth/example/lib/main.dart b/packages/local_auth/local_auth/example/lib/main.dart index 8a99ee56b485..f8e08863b564 100644 --- a/packages/local_auth/local_auth/example/lib/main.dart +++ b/packages/local_auth/local_auth/example/lib/main.dart @@ -183,11 +183,9 @@ class _MyAppState extends State { if (_isAuthenticating) ElevatedButton( onPressed: _cancelAuthentication, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Cancel Authentication'), Icon(Icons.cancel), ], @@ -198,11 +196,9 @@ class _MyAppState extends State { children: [ ElevatedButton( onPressed: _authenticate, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Authenticate'), Icon(Icons.perm_device_information), ], diff --git a/packages/local_auth/local_auth/example/pubspec.yaml b/packages/local_auth/local_auth/example/pubspec.yaml index 53c5b88dbc06..7b5fb2322eff 100644 --- a/packages/local_auth/local_auth/example/pubspec.yaml +++ b/packages/local_auth/local_auth/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth plugin. publish_to: none environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index 89ec28846fd2..f5812afa5ff5 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -6,8 +6,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.1.6 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" flutter: plugin: diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index 614d21d1bb4e..d34206069d6d 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + ## 1.0.31 * Updates androidx.fragment version to 1.5.7. diff --git a/packages/local_auth/local_auth_android/example/lib/main.dart b/packages/local_auth/local_auth_android/example/lib/main.dart index db30c01cd4f1..378ada594aa1 100644 --- a/packages/local_auth/local_auth_android/example/lib/main.dart +++ b/packages/local_auth/local_auth_android/example/lib/main.dart @@ -188,11 +188,9 @@ class _MyAppState extends State { if (_isAuthenticating) ElevatedButton( onPressed: _cancelAuthentication, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Cancel Authentication'), Icon(Icons.cancel), ], @@ -203,11 +201,9 @@ class _MyAppState extends State { children: [ ElevatedButton( onPressed: _authenticate, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Authenticate'), Icon(Icons.perm_device_information), ], diff --git a/packages/local_auth/local_auth_android/example/pubspec.yaml b/packages/local_auth/local_auth_android/example/pubspec.yaml index d5801103e818..e0be107cc926 100644 --- a/packages/local_auth/local_auth_android/example/pubspec.yaml +++ b/packages/local_auth/local_auth_android/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth_android plugin. publish_to: none environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index 0a7063683a8c..4a3e4f110350 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 1.0.31 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" flutter: plugin: diff --git a/packages/local_auth/local_auth_ios/CHANGELOG.md b/packages/local_auth/local_auth_ios/CHANGELOG.md index 2d47acf2f013..867297555d87 100644 --- a/packages/local_auth/local_auth_ios/CHANGELOG.md +++ b/packages/local_auth/local_auth_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + ## 1.1.3 * Migrates internal implementation to Pigeon. diff --git a/packages/local_auth/local_auth_ios/example/lib/main.dart b/packages/local_auth/local_auth_ios/example/lib/main.dart index b7acc99f9d7d..3e857bd88ef8 100644 --- a/packages/local_auth/local_auth_ios/example/lib/main.dart +++ b/packages/local_auth/local_auth_ios/example/lib/main.dart @@ -187,11 +187,9 @@ class _MyAppState extends State { if (_isAuthenticating) ElevatedButton( onPressed: _cancelAuthentication, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Cancel Authentication'), Icon(Icons.cancel), ], @@ -202,11 +200,9 @@ class _MyAppState extends State { children: [ ElevatedButton( onPressed: _authenticate, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Authenticate'), Icon(Icons.perm_device_information), ], diff --git a/packages/local_auth/local_auth_ios/example/pubspec.yaml b/packages/local_auth/local_auth_ios/example/pubspec.yaml index 300f2ff967ea..74d524743b6c 100644 --- a/packages/local_auth/local_auth_ios/example/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth_ios plugin. publish_to: none environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml index 000a82e26838..cfdfc0b4e5a4 100644 --- a/packages/local_auth/local_auth_ios/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 1.1.3 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" flutter: plugin: diff --git a/packages/local_auth/local_auth_windows/CHANGELOG.md b/packages/local_auth/local_auth_windows/CHANGELOG.md index d184e5509a72..ed9de2a1025c 100644 --- a/packages/local_auth/local_auth_windows/CHANGELOG.md +++ b/packages/local_auth/local_auth_windows/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT -* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. ## 1.0.8 diff --git a/packages/local_auth/local_auth_windows/example/lib/main.dart b/packages/local_auth/local_auth_windows/example/lib/main.dart index e2e7747d0e59..3f4ee19d6864 100644 --- a/packages/local_auth/local_auth_windows/example/lib/main.dart +++ b/packages/local_auth/local_auth_windows/example/lib/main.dart @@ -150,11 +150,9 @@ class _MyAppState extends State { if (_isAuthenticating) ElevatedButton( onPressed: _cancelAuthentication, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Cancel Authentication'), Icon(Icons.cancel), ], @@ -165,11 +163,9 @@ class _MyAppState extends State { children: [ ElevatedButton( onPressed: _authenticate, - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - child: Row( + child: const Row( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ Text('Authenticate'), Icon(Icons.perm_device_information), ], diff --git a/packages/local_auth/local_auth_windows/example/pubspec.yaml b/packages/local_auth/local_auth_windows/example/pubspec.yaml index 2b31020e0753..30d4ec2ae373 100644 --- a/packages/local_auth/local_auth_windows/example/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the local_auth_windows plugin. publish_to: none environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml index f0b97d3a1609..c40d11f9db0c 100644 --- a/packages/local_auth/local_auth_windows/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 1.0.8 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" flutter: plugin: diff --git a/packages/rfw/CHANGELOG.md b/packages/rfw/CHANGELOG.md index 2a6e3a4e12bd..347db107d360 100644 --- a/packages/rfw/CHANGELOG.md +++ b/packages/rfw/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. + ## 1.0.9 * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. diff --git a/packages/rfw/example/remote/lib/main.dart b/packages/rfw/example/remote/lib/main.dart index b6d409c8389e..f0b5092511f3 100644 --- a/packages/rfw/example/remote/lib/main.dart +++ b/packages/rfw/example/remote/lib/main.dart @@ -87,21 +87,13 @@ class _ExampleState extends State { }, ); } else { - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - result = Material( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors + result = const Material( child: SafeArea( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors child: Padding( - padding: const EdgeInsets.all(20.0), - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors + padding: EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, - children: const [ + children: [ Padding(padding: EdgeInsets.only(right: 100.0), child: Text('REMOTE', textAlign: TextAlign.center, style: TextStyle(letterSpacing: 12.0))), Expanded(child: DecoratedBox(decoration: FlutterLogoDecoration(style: FlutterLogoStyle.horizontal))), Padding(padding: EdgeInsets.only(left: 100.0), child: Text('WIDGETS', textAlign: TextAlign.center, style: TextStyle(letterSpacing: 12.0))), diff --git a/packages/rfw/example/remote/pubspec.yaml b/packages/rfw/example/remote/pubspec.yaml index 0b744cc987a3..a801d881cc82 100644 --- a/packages/rfw/example/remote/pubspec.yaml +++ b/packages/rfw/example/remote/pubspec.yaml @@ -4,8 +4,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/rfw/pubspec.yaml b/packages/rfw/pubspec.yaml index 5db6474642d1..c15b97011cc2 100644 --- a/packages/rfw/pubspec.yaml +++ b/packages/rfw/pubspec.yaml @@ -5,8 +5,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 1.0.9 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index ebb56fde9496..7360ed630027 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,6 +1,8 @@ ## NEXT * Updates minimum supported macOS version to 10.14. +* Fixes stale ignore: prefer_const_constructors. +* Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. ## 6.1.11 diff --git a/packages/url_launcher/url_launcher/example/lib/encoding.dart b/packages/url_launcher/url_launcher/example/lib/encoding.dart index 575eb5f42387..0875a8587269 100644 --- a/packages/url_launcher/url_launcher/example/lib/encoding.dart +++ b/packages/url_launcher/url_launcher/example/lib/encoding.dart @@ -22,17 +22,11 @@ String? encodeQueryParameters(Map params) { // #enddocregion encode-query-parameters void main() => runApp( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors - MaterialApp( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors + const MaterialApp( home: Material( - // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. - // ignore: prefer_const_constructors child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: const [ + children: [ ElevatedButton( onPressed: _composeMail, child: Text('Compose an email'), diff --git a/packages/url_launcher/url_launcher/example/pubspec.yaml b/packages/url_launcher/url_launcher/example/pubspec.yaml index e6550de37841..28c8cc5c1c1f 100644 --- a/packages/url_launcher/url_launcher/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the url_launcher plugin. publish_to: none environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 029ad6603d25..6cf68a7210a9 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -6,8 +6,8 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 6.1.11 environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" flutter: plugin: From 4515495915ba08c2223007dcf9c1d00070939601 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 8 Jun 2023 12:56:34 -0400 Subject: [PATCH 05/53] Roll Flutter from 8a5c22e282db to 6e254a3f9fb7 (12 revisions) (#4165) https://github.com/flutter/flutter/compare/8a5c22e282db...6e254a3f9fb7 2023-06-08 chillers@google.com [labeler] Set sync labels to false to stop removing labels (flutter/flutter#128446) 2023-06-08 jacksongardner@google.com Update Chrome version for testing (flutter/flutter#128447) 2023-06-08 zanderso@users.noreply.github.com Revert "Redo make inspector weakly referencing the inspected objects." (flutter/flutter#128506) 2023-06-08 sstrickl@google.com Use `--target-os` for appropriate precompiled targets. (flutter/flutter#127567) 2023-06-08 polinach@google.com Redo make inspector weakly referencing the inspected objects. (flutter/flutter#128471) 2023-06-07 engine-flutter-autoroll@skia.org Roll Flutter Engine from 1089ce6874cf to a5f7d5d75ff2 (11 revisions) (flutter/flutter#128473) 2023-06-07 gspencergoog@users.noreply.github.com Disable context menu (flutter/flutter#128365) 2023-06-07 47866232+chunhtai@users.noreply.github.com Adds vmservices to retrieve android applink settings (flutter/flutter#125998) 2023-06-07 engine-flutter-autoroll@skia.org Roll Flutter Engine from 4f4486b00be2 to 1089ce6874cf (20 revisions) (flutter/flutter#128460) 2023-06-07 leroux_bruno@yahoo.fr Fix typos 'wether' -> 'whether' (flutter/flutter#128392) 2023-06-07 aam@google.com Roll engine, patch expression evaluation (flutter/flutter#128255) 2023-06-07 engine-flutter-autoroll@skia.org Roll Packages from da72219048fc to a84b2c2ac344 (1 revision) (flutter/flutter#128444) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC rmistry@google.com,stuartmorgan@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 9ddb35ef55a1..151574d22714 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -8a5c22e282db5f45ffdef24752520894f18227b9 +6e254a3f9fb7353b043d10ed70fb9553bf84ea2d From bcddb9a1f0175018b95228a485d83a240f9f21e2 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 8 Jun 2023 14:20:35 -0400 Subject: [PATCH 06/53] [webview_flutter] Explicitly disable ATS in example apps (#4166) Our integration tests connect to a local server via HTTP; currently we are relying on ATS default behavior of allowing that for local network requests, but we should instead explicitly allow the example apps to load HTTP URLs in the webview via `NSAppTransportSecurity`. This allows integration tests to run on iOS 17, which appears to have tightened the ATS defaults. --- .../webview_flutter/example/ios/Runner/Info.plist | 5 +++++ .../webview_flutter_wkwebview/example/ios/Runner/Info.plist | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist index 6ee44fd0e2fd..ee17126a336f 100644 --- a/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist @@ -45,5 +45,10 @@ UIApplicationSupportsIndirectInputEvents + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist index 35368a316f1c..1b07552b898d 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist @@ -47,5 +47,10 @@ NSCameraUsageDescription If you want to use the camera, you have to give permission. + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + From 7d00ea7d4b7a6aa3a775e0bc3afc52417f416a5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 18:20:37 +0000 Subject: [PATCH 07/53] [image_picker]: Bump androidx.activity:activity from 1.7.0 to 1.7.1 in /packages/image_picker/image_picker_android/android (#3768) Bumps androidx.activity:activity from 1.6.1 to 1.7.1. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=androidx.activity:activity&package-manager=gradle&previous-version=1.6.1&new-version=1.7.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. --- packages/image_picker/image_picker_android/CHANGELOG.md | 4 ++++ .../image_picker/image_picker_android/android/build.gradle | 2 +- packages/image_picker/image_picker_android/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index cfb5ae4cb54a..97ccd9d57ca1 100644 --- a/packages/image_picker/image_picker_android/CHANGELOG.md +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.6+20 + +* Bumps androidx.activity:activity from 1.7.0 to 1.7.1. + ## 0.8.6+19 * Bumps androidx.core:core from 1.9.0 to 1.10.1. diff --git a/packages/image_picker/image_picker_android/android/build.gradle b/packages/image_picker/image_picker_android/android/build.gradle index 6cced2f1edf9..b64b437bda4b 100644 --- a/packages/image_picker/image_picker_android/android/build.gradle +++ b/packages/image_picker/image_picker_android/android/build.gradle @@ -41,7 +41,7 @@ android { implementation 'androidx.core:core:1.10.1' implementation 'androidx.annotation:annotation:1.3.0' implementation 'androidx.exifinterface:exifinterface:1.3.6' - implementation 'androidx.activity:activity:1.7.0' + implementation 'androidx.activity:activity:1.7.1' // org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions. // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.21")) diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index 1f044b3c50b5..8c61648db822 100755 --- a/packages/image_picker/image_picker_android/pubspec.yaml +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -3,7 +3,7 @@ description: Android implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.6+19 +version: 0.8.6+20 environment: sdk: ">=2.18.0 <4.0.0" From 852acaa9f9feabcb22580ff0349ae3d3a277330c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jun 2023 18:54:05 +0000 Subject: [PATCH 08/53] [in_app_pur]: Bump org.jetbrains.kotlin:kotlin-bom from 1.8.0 to 1.8.21 in /packages/in_app_purchase/in_app_purchase_android/android (#3839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [org.jetbrains.kotlin:kotlin-bom](https://github.com/JetBrains/kotlin) from 1.8.0 to 1.8.21.
Release notes

Sourced from org.jetbrains.kotlin:kotlin-bom's releases.

Kotlin 1.8.21

Changelog

Compiler

  • KT-57848 Native: compilation of dynamic/static library fails with Xcode 14.3
  • KT-57875 Native compilation failure: Suspend functions should be lowered out at this point, but FUN LOCAL_FUNCTION_FOR_LAMBDA
  • KT-57946 KAPT: "RuntimeException: No type for expression" with delegate

JavaScript

  • KT-57356 KJS: StackOverflowException on @JsExport with type parameters referring to one another

Tools. Commonizer

  • KT-57796 NoSuchFileException in :module-B:commonizeCInterop with Kotlin 1.8.20

Tools. Compiler plugins. Serialization

  • KT-58067 Serialization: NullPointerException caused by @Contextual property with type with generic

Tools. Gradle. JS

  • KT-57766 KJS / Gradle "Module not found: Error: Can't resolve 'kotlin-kotlin-stdlib-js-ir'" when using "useEsModules"

Tools. Kapt

  • KT-58027 Kotlin 1.8.20 kapt issue "null: KtCallExpression: build()"

Tools. Maven

  • KT-58048 Maven: "Too many source module declarations found" after upgrading to 1.8.20
  • KT-58101 'Unable to access class' in kotlin-maven-plugin after updating to Kotlin 1.8.20

Checksums

File Sha256
kotlin-compiler-1.8.21.zip 6e43c5569ad067492d04d92c28cdf8095673699d81ce460bd7270443297e8fd7
kotlin-native-linux-x86_64-1.8.21.tar.gz 0fc854641dd6d88dce3cc044bb5bc99b9035480474e23541eff03771690c68cf
kotlin-native-macos-x86_64-1.8.21.tar.gz e73bfdac0461ee0c84f25e61063c243bf8d4eb8e0ff5da250456d6a23c677fd6
kotlin-native-macos-aarch64-1.8.21.tar.gz 0a97957921ed55458f3e6cc9117643914ccf3663d8ad7eb161a4a03035a7521b
kotlin-native-windows-x86_64-1.8.21.zip f23d3288a47ee580f82f862d55452a75074fce3d26fe7e7f55e27b6015226d8d

Kotlin 1.8.20

Changelog

Analysis API

... (truncated)

Changelog

Sourced from org.jetbrains.kotlin:kotlin-bom's changelog.

1.8.21

Compiler

  • KT-57848 Native: compilation of dynamic/static library fails with Xcode 14.3
  • KT-57875 Native compilation failure: Suspend functions should be lowered out at this point, but FUN LOCAL_FUNCTION_FOR_LAMBDA
  • KT-57946 KAPT: "RuntimeException: No type for expression" with delegate

JavaScript

Tools. Compiler plugins. Serialization

Tools. Gradle. JS

  • KT-57766 KJS / Gradle "Module not found: Error: Can't resolve 'kotlin-kotlin-stdlib-js-ir'" when using "useEsModules"

Tools. Kapt

  • KT-58027 Kotlin 1.8.20 kapt issue "null: KtCallExpression: build()"

Tools. Maven

  • KT-58048 Maven: "Too many source module declarations found" after upgrading to 1.8.20
  • KT-58101 'Unable to access class' in kotlin-maven-plugin after updating to Kotlin 1.8.20

1.8.20

Analysis API

  • KT-55510 K2: Lost designation for local classes
  • KT-55191 AA: add an API to compare symbol pointers
  • KT-55487 K2: symbol pointer restoring doesn't work for static members
  • KT-55336 K2 IDE: "java.lang.IllegalStateException: Required value was null." exception while importing a compiled JPS project
  • KT-55098 AA: KtDeclarationRenderer should render a context receivers
  • KT-51181 LL API: errors for SAM with suspend function from another module
  • KT-50250 Analysis API: Implement Analysis API of KtExpression.isUsedAsExpression
  • KT-54360 KtPropertySymbol: support JvmField in javaSetterName and javaGetterName

Analysis API. FE1.0

  • KT-55825 AA FE1.0: stackoverflow when resolution to a function with a recursive type parameter

Analysis API. FIR

... (truncated)

Commits
  • ec1553a Add Changelog for 1.8.21
  • 92c7d49 [Maven] Filter duplicated source roots to avoid multiple module declarations ...
  • 035172c [KxSerialization] Fix "IllegalAccessError: Update to static final field"
  • ea2e0bd Fix maven script executor after changes in the jdk path processing
  • d44d8ea Kapt+JVM_IR: generate delegated members correctly
  • ebdbaab Correctly support nullability in type arguments for serializer<T>() intrinsic.
  • 34efee5 Don't fail if there is no serializer for type parameters of contextual serial...
  • a75271c Do not create cacheableChildSerializers unless it is necessary
  • 9f94142 [K/JS] Rework ES modules part with squashed JsImport and right renaming strat...
  • 153d7b9 [K/JS] Change strategy for implicitly exported declarations if there is a cyc...
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.jetbrains.kotlin:kotlin-bom&package-manager=gradle&previous-version=1.8.0&new-version=1.8.21)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. --- packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md | 4 ++++ .../in_app_purchase_android/android/build.gradle | 2 +- packages/in_app_purchase/in_app_purchase_android/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index 34af66f18b83..f32df46b48dd 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.0+5 + +* Bumps org.jetbrains.kotlin:kotlin-bom from 1.8.0 to 1.8.21. + ## 0.3.0+4 * Fixes unawaited_futures violations. diff --git a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle index a07a53a77d8f..3a2344bda3ee 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle +++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle @@ -60,7 +60,7 @@ dependencies { implementation 'androidx.annotation:annotation:1.6.0' // org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions. // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 - implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) + implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.21")) implementation 'com.android.billingclient:billing:5.2.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.json:json:20230227' diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index c294ad968980..2132e84abba2 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.3.0+4 +version: 0.3.0+5 environment: sdk: ">=2.18.0 <4.0.0" From f6633b20d098f4f129516e0c3ab60141e0930709 Mon Sep 17 00:00:00 2001 From: Nils Reichardt Date: Thu, 8 Jun 2023 23:57:58 +0200 Subject: [PATCH 09/53] [go_router] Update link to example for query and path parameters (#3994) This PR updates the link to the example for query and path parameters because the old link doesn't exist anymore. image --- packages/go_router/CHANGELOG.md | 5 +++++ packages/go_router/lib/src/route.dart | 2 +- packages/go_router/pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 0bba2a21f2c7..173973a67e91 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,8 @@ +## 8.0.1 + +- Fixes a link for an example in `path` documentation. + documentation. + ## 8.0.0 - **BREAKING CHANGE**: diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index 8983a7894e9c..ba656326e328 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -208,7 +208,7 @@ class GoRoute extends RouteBase { /// The query parameter are also capture during the route parsing and stored /// in [GoRouterState]. /// - /// See [Query parameters and path parameters](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/sub_routes.dart) + /// See [Query parameters and path parameters](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/path_and_query_parameters.dart) /// to learn more about parameters. final String path; diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index b71ea714404c..2d152daa8ae1 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 8.0.0 +version: 8.0.1 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 From afe2f05c1adb9df44e37b7c9c9893b0c1de77dda Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 8 Jun 2023 19:51:41 -0400 Subject: [PATCH 10/53] [image_picker] Add desktop support - platform interface (#4161) Platform interface portion of https://github.com/flutter/packages/pull/3882 Adds CameraDelegatingImagePickerPlatform and ImagePickerCameraDelegate, and supportsImageSource Part of https://github.com/flutter/flutter/issues/102115 Part of https://github.com/flutter/flutter/issues/102320 Part of https://github.com/flutter/flutter/issues/85100 --- .../CHANGELOG.md | 10 +- .../image_picker_platform.dart | 73 +++++++++++- .../lib/src/types/camera_delegate.dart | 53 +++++++++ .../lib/src/types/types.dart | 1 + .../pubspec.yaml | 2 +- .../test/image_picker_platform_test.dart | 104 ++++++++++++++++++ ... => method_channel_image_picker_test.dart} | 0 7 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 packages/image_picker/image_picker_platform_interface/lib/src/types/camera_delegate.dart create mode 100644 packages/image_picker/image_picker_platform_interface/test/image_picker_platform_test.dart rename packages/image_picker/image_picker_platform_interface/test/{new_method_channel_image_picker_test.dart => method_channel_image_picker_test.dart} (100%) diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 0e50cd22ecfc..f93b6ec181ec 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.7.0 + +* Adds `CameraDelegatingImagePickerPlatform` as a base class for platform + implementations that don't support `ImageSource.camera`, but allow for an- + implementation to be provided at the application level via implementation + of `CameraDelegatingImagePickerPlatform`. +* Adds `supportsImageSource` to check source support at runtime. + ## 2.6.4 * Adds compatibility with `http` 1.0. @@ -32,7 +40,7 @@ * Adds `requestFullMetadata` option that allows disabling extra permission requests on certain platforms. * Moves optional image picking parameters to `ImagePickerOptions` class. -* Minor fixes for new analysis options. +* Minor fixes for new analysis options. ## 2.4.4 diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart index c8942cd2da0e..e01caca14616 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart @@ -32,8 +32,6 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// Platform-specific plugins should set this with their own platform-specific /// class that extends [ImagePickerPlatform] when they register themselves. - // TODO(amirh): Extract common platform interface logic. - // https://github.com/flutter/flutter/issues/43368 static set instance(ImagePickerPlatform instance) { PlatformInterface.verify(instance, _token); _instance = instance; @@ -305,4 +303,75 @@ abstract class ImagePickerPlatform extends PlatformInterface { ); return pickedImages ?? []; } + + /// Returns true if the implementation supports [source]. + /// + /// Defaults to true for the original image sources, `gallery` and `camera`, + /// for backwards compatibility. + bool supportsImageSource(ImageSource source) { + return source == ImageSource.gallery || source == ImageSource.camera; + } +} + +/// A base class for an [ImagePickerPlatform] implementation that does not +/// directly support [ImageSource.camera], but supports delegating to a +/// provided [ImagePickerCameraDelegate]. +abstract class CameraDelegatingImagePickerPlatform extends ImagePickerPlatform { + /// A delegate to respond to calls that use [ImageSource.camera]. + /// + /// When it is null, attempting to use [ImageSource.camera] will throw a + /// [StateError]. + ImagePickerCameraDelegate? cameraDelegate; + + @override + bool supportsImageSource(ImageSource source) { + if (source == ImageSource.camera) { + return cameraDelegate != null; + } + return super.supportsImageSource(source); + } + + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + if (source == ImageSource.camera) { + final ImagePickerCameraDelegate? delegate = cameraDelegate; + if (delegate == null) { + throw StateError( + 'This implementation of ImagePickerPlatform requires a ' + '"cameraDelegate" in order to use ImageSource.camera'); + } + return delegate.takePhoto( + options: ImagePickerCameraDelegateOptions( + preferredCameraDevice: options.preferredCameraDevice, + )); + } + return super.getImageFromSource(source: source, options: options); + } + + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + if (source == ImageSource.camera) { + final ImagePickerCameraDelegate? delegate = cameraDelegate; + if (delegate == null) { + throw StateError( + 'This implementation of ImagePickerPlatform requires a ' + '"cameraDelegate" in order to use ImageSource.camera'); + } + return delegate.takeVideo( + options: ImagePickerCameraDelegateOptions( + preferredCameraDevice: preferredCameraDevice, + maxVideoDuration: maxDuration)); + } + return super.getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_delegate.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_delegate.dart new file mode 100644 index 000000000000..39584c923b03 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_delegate.dart @@ -0,0 +1,53 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart' show immutable; + +import 'camera_device.dart'; + +/// Options for [ImagePickerCameraDelegate] methods. +/// +/// New options may be added in the future. +@immutable +class ImagePickerCameraDelegateOptions { + /// Creates a new set of options for taking an image or video. + const ImagePickerCameraDelegateOptions({ + this.preferredCameraDevice = CameraDevice.rear, + this.maxVideoDuration, + }); + + /// The camera device to default to, if available. + /// + /// Defaults to [CameraDevice.rear]. + final CameraDevice preferredCameraDevice; + + /// The maximum duration to allow when recording a video. + /// + /// Defaults to null, meaning no maximum duration. + final Duration? maxVideoDuration; +} + +/// A delegate for `ImagePickerPlatform` implementations that do not provide +/// a camera implementation, or that have a default but allow substituting an +/// alternate implementation. +abstract class ImagePickerCameraDelegate { + /// Takes a photo with the given [options] and returns an [XFile] to the + /// resulting image file. + /// + /// Returns null if the photo could not be taken, or the user cancelled. + Future takePhoto({ + ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions(), + }); + + /// Records a video with the given [options] and returns an [XFile] to the + /// resulting video file. + /// + /// Returns null if the video could not be recorded, or the user cancelled. + Future takeVideo({ + ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions(), + }); +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart index fbe12e8e825a..fcb76ccefa2f 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'camera_delegate.dart'; export 'camera_device.dart'; export 'image_options.dart'; export 'image_picker_options.dart'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 9c8a55ad96ad..3f1e523453f8 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/image_picker/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.6.4 +version: 2.7.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/image_picker/image_picker_platform_interface/test/image_picker_platform_test.dart b/packages/image_picker/image_picker_platform_interface/test/image_picker_platform_test.dart new file mode 100644 index 000000000000..89dc1ae382da --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/image_picker_platform_test.dart @@ -0,0 +1,104 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +void main() { + group('ImagePickerPlatform', () { + test('supportsImageSource defaults to true for original values', () async { + final ImagePickerPlatform implementation = FakeImagePickerPlatform(); + + expect(implementation.supportsImageSource(ImageSource.camera), true); + expect(implementation.supportsImageSource(ImageSource.gallery), true); + }); + }); + + group('CameraDelegatingImagePickerPlatform', () { + test( + 'supportsImageSource returns false for camera when there is no delegate', + () async { + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + + expect(implementation.supportsImageSource(ImageSource.camera), false); + }); + + test('supportsImageSource returns true for camera when there is a delegate', + () async { + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + implementation.cameraDelegate = FakeCameraDelegate(); + + expect(implementation.supportsImageSource(ImageSource.camera), true); + }); + + test('getImageFromSource for camera throws if delegate is not set', + () async { + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + + await expectLater( + implementation.getImageFromSource(source: ImageSource.camera), + throwsStateError); + }); + + test('getVideo for camera throws if delegate is not set', () async { + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + + await expectLater(implementation.getVideo(source: ImageSource.camera), + throwsStateError); + }); + + test('getImageFromSource for camera calls delegate if set', () async { + const String fakePath = '/tmp/foo'; + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + implementation.cameraDelegate = + FakeCameraDelegate(result: XFile(fakePath)); + + expect( + (await implementation.getImageFromSource(source: ImageSource.camera))! + .path, + fakePath); + }); + + test('getVideo for camera calls delegate if set', () async { + const String fakePath = '/tmp/foo'; + final FakeCameraDelegatingImagePickerPlatform implementation = + FakeCameraDelegatingImagePickerPlatform(); + implementation.cameraDelegate = + FakeCameraDelegate(result: XFile(fakePath)); + + expect((await implementation.getVideo(source: ImageSource.camera))!.path, + fakePath); + }); + }); +} + +class FakeImagePickerPlatform extends ImagePickerPlatform {} + +class FakeCameraDelegatingImagePickerPlatform + extends CameraDelegatingImagePickerPlatform {} + +class FakeCameraDelegate extends ImagePickerCameraDelegate { + FakeCameraDelegate({this.result}); + + XFile? result; + + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } +} diff --git a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart similarity index 100% rename from packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart rename to packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart From cdb9bc6fe9e8b73e4717eb930ee787c4454adcfb Mon Sep 17 00:00:00 2001 From: Valentin Vignal <32538273+ValentinVignal@users.noreply.github.com> Date: Fri, 9 Jun 2023 23:41:18 +0800 Subject: [PATCH 11/53] [go_router_builder] Include required and positional query parameters in the location (#4163) Fixes https://github.com/flutter/flutter/issues/128483 --- packages/go_router_builder/CHANGELOG.md | 4 ++++ packages/go_router_builder/lib/src/route_config.dart | 4 ++-- packages/go_router_builder/pubspec.yaml | 2 +- .../test/test_inputs/_go_router_builder_test_input.dart | 6 ++++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index 81a4e7db62f0..ce6210d043fd 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.1 + +* Fixes a bug that the required/positional parameters are not added to query parameters correctly. + ## 2.1.0 * Supports required/positional parameters that are not in the path. diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index 3e53cb1c427e..693eb4a42cf3 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -441,7 +441,7 @@ GoRouteData.\$route( late final List _ctorParams = _ctor.parameters.where((ParameterElement element) { - if (element.isRequired && !element.isExtraField) { + if (_pathParams.contains(element.name)) { return true; } return false; @@ -449,7 +449,7 @@ GoRouteData.\$route( late final List _ctorQueryParams = _ctor.parameters .where((ParameterElement element) => - element.isOptional && !element.isExtraField) + !_pathParams.contains(element.name) && !element.isExtraField) .toList(); ConstructorElement get _ctor { diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 027b79ea2c6f..9f213c1508d2 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -2,7 +2,7 @@ name: go_router_builder description: >- A builder that supports generated strongly-typed route helpers for package:go_router -version: 2.1.0 +version: 2.1.1 repository: https://github.com/flutter/packages/tree/main/packages/go_router_builder issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router_builder%22 diff --git a/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart b/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart index 016d2c86518a..b27b1ea91002 100644 --- a/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart +++ b/packages/go_router_builder/test/test_inputs/_go_router_builder_test_input.dart @@ -68,6 +68,9 @@ extension $NullableRequiredParamNotInPathExtension String get location => GoRouteData.$location( 'bob', + queryParams: { + if (id != null) 'id': id!.toString(), + }, ); void go(BuildContext context) => context.go(location); @@ -108,6 +111,9 @@ extension $NonNullableRequiredParamNotInPathExtension String get location => GoRouteData.$location( 'bob', + queryParams: { + 'id': id.toString(), + }, ); void go(BuildContext context) => context.go(location); From 914d120da12fba458c020210727831c31bd71041 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 9 Jun 2023 12:15:08 -0400 Subject: [PATCH 12/53] Roll Flutter from 6e254a3f9fb7 to da127f15ad54 (28 revisions) (#4170) https://github.com/flutter/flutter/compare/6e254a3f9fb7...da127f15ad54 2023-06-09 hans.muller@gmail.com Updated material button theme tests for Material3 (flutter/flutter#128543) 2023-06-09 engine-flutter-autoroll@skia.org Roll Flutter Engine from cb93477008d6 to 93afba901b3b (2 revisions) (flutter/flutter#128573) 2023-06-09 6655696+guidezpl@users.noreply.github.com Improve defaults generation with logging, stats, and token validation (flutter/flutter#128244) 2023-06-09 whesse@google.com [testing] Make the FLUTTER_STORAGE_BASE_URL warning non-fatal (flutter/flutter#128335) 2023-06-09 danny@tuppeny.com [flutter_tools] [DAP] Don't try to restart/reload if app hasn't started yet (flutter/flutter#128267) 2023-06-09 engine-flutter-autoroll@skia.org Roll Flutter Engine from 8f9e608d39ab to cb93477008d6 (3 revisions) (flutter/flutter#128568) 2023-06-09 tessertaha@gmail.com Replace `MaterialButton` from test classes (flutter/flutter#128466) 2023-06-09 tessertaha@gmail.com Fix `showBottomSheet` doesn't remove scrim when draggable sheet is dismissed (flutter/flutter#128455) 2023-06-09 engine-flutter-autoroll@skia.org Manual roll Flutter Engine from a5f7d5d75ff2 to 8f9e608d39ab (31 revisions) (flutter/flutter#128554) 2023-06-09 ychris@google.com Revert "test owners: cyanglaz -> vashworth" (flutter/flutter#128462) 2023-06-09 43054281+camsim99@users.noreply.github.com [Android] Bump integration tests using `compileSdkVersion` 31 to 33 (flutter/flutter#128072) 2023-06-09 dkwingsmt@users.noreply.github.com Remove single view assumption from MouseTracker, and unify its hit testing code flow (flutter/flutter#127060) 2023-06-09 christopherfujino@gmail.com [flutter_tools] Precache after channel switch (flutter/flutter#118129) 2023-06-08 leigha.jarett@gmail.com Adding migration guide for Material 3 colors (flutter/flutter#128429) 2023-06-08 gspencergoog@users.noreply.github.com Add `AppLifecycleListener`, with support for application exit handling (flutter/flutter#123274) 2023-06-08 thkim1011@users.noreply.github.com Sliver Main Axis Group (flutter/flutter#126596) 2023-06-08 31859944+LongCatIsLooong@users.noreply.github.com Reduce `_DoubleClampVisitor` false positives (flutter/flutter#128539) 2023-06-08 leigha.jarett@gmail.com Advise developers to use OverflowBar instead of ButtonBar (flutter/flutter#128437) 2023-06-08 jacksongardner@google.com Reland "Migrate benchmarks to package:web" (flutter/flutter#128266) 2023-06-08 53684884+mhbdev@users.noreply.github.com Navigator.pop before PopupMenuItem onTap call (flutter/flutter#127446) 2023-06-08 leroux_bruno@yahoo.fr Fix navigation rail with long labels misplaced highlights (flutter/flutter#128324) 2023-06-08 tessertaha@gmail.com Update `chip.dart` to use set of `MaterialState` (flutter/flutter#128507) 2023-06-08 jcollins@google.com Update flutter to dartdoc 6.3.0 and hide Icons implementation from doc pages (flutter/flutter#128442) 2023-06-08 31859944+LongCatIsLooong@users.noreply.github.com Disable blinking cursor when `EditableText.showCursor` is false (flutter/flutter#127562) 2023-06-08 41930132+hellohuanlin@users.noreply.github.com [floating_cursor_selection]add more comments on the tricky part (flutter/flutter#127227) 2023-06-08 goderbauer@google.com Move RenderObjectElement.updateChildren to Element (flutter/flutter#128458) 2023-06-08 goderbauer@google.com Fix PointerEventConverter doc (flutter/flutter#128452) 2023-06-08 engine-flutter-autoroll@skia.org Roll Packages from a84b2c2ac344 to e13b8c43386a (9 revisions) (flutter/flutter#128508) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC rmistry@google.com,stuartmorgan@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 151574d22714..dd97cf9799a3 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -6e254a3f9fb7353b043d10ed70fb9553bf84ea2d +da127f15ad54f3396e475540dbfb3fda790a0e1d From ecf2b68093b22d7d84630f0ba0d01584864360ec Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 9 Jun 2023 17:40:38 -0400 Subject: [PATCH 13/53] [image_picker] Add desktop support - implementations (#4172) Platform implementation portion of https://github.com/flutter/packages/pull/3882 Updates the Windows implementation to use the new base class for camera delegation, and creates new macOS and Linux implementations that are near-duplicates. These are separate packages, rather than a single shared package, because it's likely that they will diverge over time (e.g., the TODO for macOS to use a system image picker control on newer versions of macOS), and the amount of code that could be shared is minimal anyway. Part of https://github.com/flutter/flutter/issues/102115 Part of https://github.com/flutter/flutter/issues/102320 Part of https://github.com/flutter/flutter/issues/85100 --- .../image_picker/image_picker_linux/AUTHORS | 7 + .../image_picker_linux/CHANGELOG.md | 3 + .../image_picker/image_picker_linux/LICENSE | 25 + .../image_picker/image_picker_linux/README.md | 27 + .../image_picker_linux/example/README.md | 9 + .../image_picker_linux/example/lib/main.dart | 422 +++++++++++++ .../example/linux/.gitignore | 1 + .../example/linux/CMakeLists.txt | 138 +++++ .../example/linux/flutter/CMakeLists.txt | 88 +++ .../linux/flutter/generated_plugins.cmake | 24 + .../image_picker_linux/example/linux/main.cc | 10 + .../example/linux/my_application.cc | 111 ++++ .../example/linux/my_application.h | 22 + .../image_picker_linux/example/pubspec.yaml | 28 + .../lib/image_picker_linux.dart | 157 +++++ .../image_picker_linux/pubspec.yaml | 29 + .../test/image_picker_linux_test.dart | 148 +++++ .../test/image_picker_linux_test.mocks.dart | 120 ++++ .../image_picker/image_picker_macos/AUTHORS | 7 + .../image_picker_macos/CHANGELOG.md | 3 + .../image_picker/image_picker_macos/LICENSE | 25 + .../image_picker/image_picker_macos/README.md | 38 ++ .../image_picker_macos/example/README.md | 9 + .../image_picker_macos/example/lib/main.dart | 422 +++++++++++++ .../example/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../image_picker_macos/example/macos/Podfile | 40 ++ .../macos/Runner.xcodeproj/project.pbxproj | 573 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 +++ .../macos/Runner/Base.lproj/MainMenu.xib | 343 +++++++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 14 + .../example/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 19 + .../example/macos/Runner/Release.entitlements | 10 + .../image_picker_macos/example/pubspec.yaml | 28 + .../lib/image_picker_macos.dart | 162 +++++ .../image_picker_macos/pubspec.yaml | 29 + .../test/image_picker_macos_test.dart | 154 +++++ .../test/image_picker_macos_test.mocks.dart | 120 ++++ .../image_picker/image_picker_windows/AUTHORS | 2 +- .../image_picker_windows/CHANGELOG.md | 2 +- .../image_picker_windows/README.md | 21 +- .../example/lib/main.dart | 70 ++- .../image_picker_windows/example/pubspec.yaml | 2 +- .../lib/image_picker_windows.dart | 119 ++-- .../image_picker_windows/pubspec.yaml | 6 +- .../test/image_picker_windows_test.dart | 68 ++- script/configs/exclude_integration_linux.yaml | 1 + script/configs/exclude_integration_macos.yaml | 1 + 59 files changed, 3806 insertions(+), 116 deletions(-) create mode 100644 packages/image_picker/image_picker_linux/AUTHORS create mode 100644 packages/image_picker/image_picker_linux/CHANGELOG.md create mode 100644 packages/image_picker/image_picker_linux/LICENSE create mode 100644 packages/image_picker/image_picker_linux/README.md create mode 100644 packages/image_picker/image_picker_linux/example/README.md create mode 100644 packages/image_picker/image_picker_linux/example/lib/main.dart create mode 100644 packages/image_picker/image_picker_linux/example/linux/.gitignore create mode 100644 packages/image_picker/image_picker_linux/example/linux/CMakeLists.txt create mode 100644 packages/image_picker/image_picker_linux/example/linux/flutter/CMakeLists.txt create mode 100644 packages/image_picker/image_picker_linux/example/linux/flutter/generated_plugins.cmake create mode 100644 packages/image_picker/image_picker_linux/example/linux/main.cc create mode 100644 packages/image_picker/image_picker_linux/example/linux/my_application.cc create mode 100644 packages/image_picker/image_picker_linux/example/linux/my_application.h create mode 100644 packages/image_picker/image_picker_linux/example/pubspec.yaml create mode 100644 packages/image_picker/image_picker_linux/lib/image_picker_linux.dart create mode 100644 packages/image_picker/image_picker_linux/pubspec.yaml create mode 100644 packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart create mode 100644 packages/image_picker/image_picker_linux/test/image_picker_linux_test.mocks.dart create mode 100644 packages/image_picker/image_picker_macos/AUTHORS create mode 100644 packages/image_picker/image_picker_macos/CHANGELOG.md create mode 100644 packages/image_picker/image_picker_macos/LICENSE create mode 100644 packages/image_picker/image_picker_macos/README.md create mode 100644 packages/image_picker/image_picker_macos/example/README.md create mode 100644 packages/image_picker/image_picker_macos/example/lib/main.dart create mode 100644 packages/image_picker/image_picker_macos/example/macos/.gitignore create mode 100644 packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 packages/image_picker/image_picker_macos/example/macos/Podfile create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/AppDelegate.swift create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Debug.xcconfig create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Release.xcconfig create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/DebugProfile.entitlements create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Info.plist create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/MainFlutterWindow.swift create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Release.entitlements create mode 100644 packages/image_picker/image_picker_macos/example/pubspec.yaml create mode 100644 packages/image_picker/image_picker_macos/lib/image_picker_macos.dart create mode 100644 packages/image_picker/image_picker_macos/pubspec.yaml create mode 100644 packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart create mode 100644 packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart diff --git a/packages/image_picker/image_picker_linux/AUTHORS b/packages/image_picker/image_picker_linux/AUTHORS new file mode 100644 index 000000000000..26e81c7fb254 --- /dev/null +++ b/packages/image_picker/image_picker_linux/AUTHORS @@ -0,0 +1,7 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +Alexandre Zollinger Chohfi diff --git a/packages/image_picker/image_picker_linux/CHANGELOG.md b/packages/image_picker/image_picker_linux/CHANGELOG.md new file mode 100644 index 000000000000..d3bfbf901bbd --- /dev/null +++ b/packages/image_picker/image_picker_linux/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.2.0 + +* Implements initial Linux support. diff --git a/packages/image_picker/image_picker_linux/LICENSE b/packages/image_picker/image_picker_linux/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/image_picker/image_picker_linux/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/image_picker/image_picker_linux/README.md b/packages/image_picker/image_picker_linux/README.md new file mode 100644 index 000000000000..1f1833e81e62 --- /dev/null +++ b/packages/image_picker/image_picker_linux/README.md @@ -0,0 +1,27 @@ +# image\_picker\_linux + +A Linux implementation of [`image_picker`][1]. + +## Limitations + +`ImageSource.camera` is not supported unless a `cameraDelegate` is set. + +### pickImage() +The arguments `maxWidth`, `maxHeight`, and `imageQuality` are not currently supported. + +### pickVideo() +The argument `maxDuration` is not currently supported. + +## Usage + +### Import the package + +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +[1]: https://pub.dev/packages/image_picker +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/image_picker/image_picker_linux/example/README.md b/packages/image_picker/image_picker_linux/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/image_picker/image_picker_linux/example/lib/main.dart b/packages/image_picker/image_picker_linux/example/lib/main.dart new file mode 100644 index 000000000000..9e22c716a2e0 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/lib/main.dart @@ -0,0 +1,422 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Image Picker Demo', + home: MyHomePage(title: 'Image Picker Example'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, this.title}); + + final String? title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + List? _imageFileList; + + // This must be called from within a setState() callback + void _setImageFileListFromFile(XFile? value) { + _imageFileList = value == null ? null : [value]; + } + + dynamic _pickImageError; + bool _isVideo = false; + + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; + + final ImagePickerPlatform _picker = ImagePickerPlatform.instance; + final TextEditingController maxWidthController = TextEditingController(); + final TextEditingController maxHeightController = TextEditingController(); + final TextEditingController qualityController = TextEditingController(); + + Future _playVideo(XFile? file) async { + if (file != null && mounted) { + await _disposeVideoController(); + final VideoPlayerController controller = + VideoPlayerController.file(File(file.path)); + _controller = controller; + await controller.setVolume(1.0); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); + setState(() {}); + } + } + + Future _handleMultiImagePicked(BuildContext context) async { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List? pickedFileList = await _picker.getMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + + Future _handleSingleImagePicked( + BuildContext context, ImageSource source) async { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + + Future _onImageButtonPressed(ImageSource source, + {required BuildContext context, bool isMultiImage = false}) async { + if (_controller != null) { + await _controller!.setVolume(0.0); + } + if (context.mounted) { + if (_isVideo) { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + await _playVideo(file); + } else if (isMultiImage) { + await _handleMultiImagePicked(context); + } else { + await _handleSingleImagePicked(context, source); + } + } + } + + @override + void deactivate() { + if (_controller != null) { + _controller!.setVolume(0.0); + _controller!.pause(); + } + super.deactivate(); + } + + @override + void dispose() { + _disposeVideoController(); + maxWidthController.dispose(); + maxHeightController.dispose(); + qualityController.dispose(); + super.dispose(); + } + + Future _disposeVideoController() async { + if (_toBeDisposed != null) { + await _toBeDisposed!.dispose(); + } + _toBeDisposed = _controller; + _controller = null; + } + + Widget _previewVideo() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_controller == null) { + return const Text( + 'You have not yet picked a video', + textAlign: TextAlign.center, + ); + } + return Padding( + padding: const EdgeInsets.all(10.0), + child: AspectRatioVideo(_controller), + ); + } + + Widget _previewImages() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_imageFileList != null) { + return Semantics( + label: 'image_picker_example_picked_images', + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (BuildContext context, int index) { + return Semantics( + label: 'image_picker_example_picked_image', + child: Image.file(File(_imageFileList![index].path)), + ); + }, + itemCount: _imageFileList!.length, + ), + ); + } else if (_pickImageError != null) { + return Text( + 'Pick image error: $_pickImageError', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + + Widget _handlePreview() { + if (_isVideo) { + return _previewVideo(); + } else { + return _previewImages(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title!), + ), + body: Center( + child: _handlePreview(), + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Semantics( + label: 'image_picker_example_from_gallery', + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'image0', + tooltip: 'Pick Image from gallery', + child: const Icon(Icons.photo), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); + }, + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), + ), + ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), + ), + ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), + ], + ), + ); + } + + Text? _getRetrieveErrorWidget() { + if (_retrieveDataError != null) { + final Text result = Text(_retrieveDataError!); + _retrieveDataError = null; + return result; + } + return null; + } + + Future _displayPickImageDialog( + BuildContext context, OnPickImageCallback onPick) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Add optional parameters'), + content: Column( + children: [ + TextField( + controller: maxWidthController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), + ), + TextField( + controller: maxHeightController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), + ), + TextField( + controller: qualityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), + ), + ], + ), + actions: [ + TextButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('PICK'), + onPressed: () { + final double? width = maxWidthController.text.isNotEmpty + ? double.parse(maxWidthController.text) + : null; + final double? height = maxHeightController.text.isNotEmpty + ? double.parse(maxHeightController.text) + : null; + final int? quality = qualityController.text.isNotEmpty + ? int.parse(qualityController.text) + : null; + onPick(width, height, quality); + Navigator.of(context).pop(); + }), + ], + ); + }); + } +} + +typedef OnPickImageCallback = void Function( + double? maxWidth, double? maxHeight, int? quality); + +class AspectRatioVideo extends StatefulWidget { + const AspectRatioVideo(this.controller, {super.key}); + + final VideoPlayerController? controller; + + @override + AspectRatioVideoState createState() => AspectRatioVideoState(); +} + +class AspectRatioVideoState extends State { + VideoPlayerController? get controller => widget.controller; + bool initialized = false; + + void _onVideoControllerUpdate() { + if (!mounted) { + return; + } + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + controller!.addListener(_onVideoControllerUpdate); + } + + @override + void dispose() { + controller!.removeListener(_onVideoControllerUpdate); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (initialized) { + return Center( + child: AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), + ), + ); + } else { + return Container(); + } + } +} diff --git a/packages/image_picker/image_picker_linux/example/linux/.gitignore b/packages/image_picker/image_picker_linux/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/image_picker/image_picker_linux/example/linux/CMakeLists.txt b/packages/image_picker/image_picker_linux/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..1fbfa72731c0 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "dev.flutter.plugins.imagePickerExample") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/image_picker/image_picker_linux/example/linux/flutter/CMakeLists.txt b/packages/image_picker/image_picker_linux/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..d5bd01648a96 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/image_picker/image_picker_linux/example/linux/flutter/generated_plugins.cmake b/packages/image_picker/image_picker_linux/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..2db3c22ae228 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/image_picker/image_picker_linux/example/linux/main.cc b/packages/image_picker/image_picker_linux/example/linux/main.cc new file mode 100644 index 000000000000..1507d02825e7 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/main.cc @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/image_picker/image_picker_linux/example/linux/my_application.cc b/packages/image_picker/image_picker_linux/example/linux/my_application.cc new file mode 100644 index 000000000000..3a67810f5612 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/my_application.cc @@ -0,0 +1,111 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/packages/image_picker/image_picker_linux/example/linux/my_application.h b/packages/image_picker/image_picker_linux/example/linux/my_application.h new file mode 100644 index 000000000000..6e9f0c3ff665 --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/linux/my_application.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/image_picker/image_picker_linux/example/pubspec.yaml b/packages/image_picker/image_picker_linux/example/pubspec.yaml new file mode 100644 index 000000000000..54beb765641c --- /dev/null +++ b/packages/image_picker/image_picker_linux/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: example +description: Example for image_picker_linux implementation. +publish_to: 'none' +version: 1.0.0 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + image_picker_linux: + # When depending on this package from a real application you should use: + # image_picker_linux: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: .. + image_picker_platform_interface: ^2.7.0 + video_player: ^2.1.4 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart b/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart new file mode 100644 index 000000000000..f932a0211721 --- /dev/null +++ b/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart @@ -0,0 +1,157 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_linux/file_selector_linux.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +/// The Linux implementation of [ImagePickerPlatform]. +/// +/// This class implements the `package:image_picker` functionality for +/// Linux. +class ImagePickerLinux extends CameraDelegatingImagePickerPlatform { + /// Constructs a platform implementation. + ImagePickerLinux(); + + /// The file selector used to prompt the user to select images or videos. + @visibleForTesting + static FileSelectorPlatform fileSelector = FileSelectorLinux(); + + /// Registers this class as the default instance of [ImagePickerPlatform]. + static void registerWith() { + ImagePickerPlatform.instance = ImagePickerLinux(); + } + + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getImageFromSource. + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final XFile? file = await getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice)); + if (file != null) { + return PickedFile(file.path); + } + return null; + } + + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getVideo. + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final XFile? file = await getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + if (file != null) { + return PickedFile(file.path); + } + return null; + } + + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getImageFromSource. + @override + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + return getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice)); + } + + // [ImagePickerOptions] options are not currently supported. If any + // of its fields are set, they will be silently ignored. + // + // If source is `ImageSource.camera`, a `StateError` will be thrown + // unless a [cameraDelegate] is set. + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + switch (source) { + case ImageSource.camera: + return super.getImageFromSource(source: source); + case ImageSource.gallery: + const XTypeGroup typeGroup = + XTypeGroup(label: 'Images', mimeTypes: ['image/*']); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; + } + // Ensure that there's a fallback in case a new source is added. + // ignore: dead_code + throw UnimplementedError('Unknown ImageSource: $source'); + } + + // `preferredCameraDevice` and `maxDuration` arguments are not currently + // supported. If either of these arguments are supplied, they will be silently + // ignored. + // + // If source is `ImageSource.camera`, a `StateError` will be thrown + // unless a [cameraDelegate] is set. + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + switch (source) { + case ImageSource.camera: + return super.getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + case ImageSource.gallery: + const XTypeGroup typeGroup = + XTypeGroup(label: 'Videos', mimeTypes: ['video/*']); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; + } + // Ensure that there's a fallback in case a new source is added. + // ignore: dead_code + throw UnimplementedError('Unknown ImageSource: $source'); + } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently + // supported. If any of these arguments are supplied, they will be silently + // ignored. + @override + Future> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + const XTypeGroup typeGroup = + XTypeGroup(label: 'Images', mimeTypes: ['image/*']); + final List files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + return files; + } +} diff --git a/packages/image_picker/image_picker_linux/pubspec.yaml b/packages/image_picker/image_picker_linux/pubspec.yaml new file mode 100644 index 000000000000..dcfd6758ad79 --- /dev/null +++ b/packages/image_picker/image_picker_linux/pubspec.yaml @@ -0,0 +1,29 @@ +name: image_picker_linux +description: Linux platform implementation of image_picker +repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_linux +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 +version: 0.2.0 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +flutter: + plugin: + implements: image_picker + platforms: + linux: + dartPluginClass: ImagePickerLinux + +dependencies: + file_selector_linux: ^0.9.1+3 + file_selector_platform_interface: ^2.2.0 + flutter: + sdk: flutter + image_picker_platform_interface: ^2.7.0 + +dev_dependencies: + build_runner: ^2.1.5 + flutter_test: + sdk: flutter + mockito: 5.4.1 diff --git a/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart new file mode 100644 index 000000000000..32c3d4509142 --- /dev/null +++ b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart @@ -0,0 +1,148 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_linux/image_picker_linux.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'image_picker_linux_test.mocks.dart'; + +@GenerateMocks([FileSelectorPlatform]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Returns the captured type groups from a mock call result, assuming that + // exactly one call was made and only the type groups were captured. + List capturedTypeGroups(VerificationResult result) { + return result.captured.single as List; + } + + late ImagePickerLinux plugin; + late MockFileSelectorPlatform mockFileSelectorPlatform; + + setUp(() { + plugin = ImagePickerLinux(); + mockFileSelectorPlatform = MockFileSelectorPlatform(); + + when(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .thenAnswer((_) async => null); + + when(mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .thenAnswer((_) async => List.empty()); + + ImagePickerLinux.fileSelector = mockFileSelectorPlatform; + }); + + test('registered instance', () { + ImagePickerLinux.registerWith(); + expect(ImagePickerPlatform.instance, isA()); + }); + + group('images', () { + test('pickImage passes the accepted type groups correctly', () async { + await plugin.pickImage(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['image/*']); + }); + + test('getImage passes the accepted type groups correctly', () async { + await plugin.getImage(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['image/*']); + }); + + test('getImageFromSource passes the accepted type groups correctly', + () async { + await plugin.getImageFromSource(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['image/*']); + }); + + test('getImageFromSource calls delegate when source is camera', () async { + const String fakePath = '/tmp/foo'; + plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); + expect( + (await plugin.getImageFromSource(source: ImageSource.camera))!.path, + fakePath); + }); + + test( + 'getImageFromSource throws StateError when source is camera with no delegate', + () async { + await expectLater(plugin.getImageFromSource(source: ImageSource.camera), + throwsStateError); + }); + + test('getMultiImage passes the accepted type groups correctly', () async { + await plugin.getMultiImage(); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['image/*']); + }); + }); + + group('videos', () { + test('pickVideo passes the accepted type groups correctly', () async { + await plugin.pickVideo(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['video/*']); + }); + + test('getVideo passes the accepted type groups correctly', () async { + await plugin.getVideo(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].mimeTypes, ['video/*']); + }); + + test('getVideo calls delegate when source is camera', () async { + const String fakePath = '/tmp/foo'; + plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); + expect( + (await plugin.getVideo(source: ImageSource.camera))!.path, fakePath); + }); + + test('getVideo throws StateError when source is camera with no delegate', + () async { + await expectLater( + plugin.getVideo(source: ImageSource.camera), throwsStateError); + }); + }); +} + +class FakeCameraDelegate extends ImagePickerCameraDelegate { + FakeCameraDelegate({this.result}); + + XFile? result; + + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } +} diff --git a/packages/image_picker/image_picker_linux/test/image_picker_linux_test.mocks.dart b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.mocks.dart new file mode 100644 index 000000000000..6cde8261f501 --- /dev/null +++ b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.mocks.dart @@ -0,0 +1,120 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in image_picker_linux/test/image_picker_linux_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart' + as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [FileSelectorPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileSelectorPlatform extends _i1.Mock + implements _i2.FileSelectorPlatform { + MockFileSelectorPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.XFile?> openFile({ + List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #openFile, + [], + { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future<_i2.XFile?>.value(), + ) as _i3.Future<_i2.XFile?>); + @override + _i3.Future> openFiles({ + List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #openFiles, + [], + { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future>.value(<_i2.XFile>[]), + ) as _i3.Future>); + @override + _i3.Future getSavePath({ + List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #getSavePath, + [], + { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #suggestedName: suggestedName, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #getDirectoryPath, + [], + { + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #getDirectoryPaths, + [], + { + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future>.value([]), + ) as _i3.Future>); +} diff --git a/packages/image_picker/image_picker_macos/AUTHORS b/packages/image_picker/image_picker_macos/AUTHORS new file mode 100644 index 000000000000..26e81c7fb254 --- /dev/null +++ b/packages/image_picker/image_picker_macos/AUTHORS @@ -0,0 +1,7 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +Alexandre Zollinger Chohfi diff --git a/packages/image_picker/image_picker_macos/CHANGELOG.md b/packages/image_picker/image_picker_macos/CHANGELOG.md new file mode 100644 index 000000000000..94ce98bd3ab1 --- /dev/null +++ b/packages/image_picker/image_picker_macos/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.2.0 + +* Implements initial macOS support. diff --git a/packages/image_picker/image_picker_macos/LICENSE b/packages/image_picker/image_picker_macos/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/image_picker/image_picker_macos/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/image_picker/image_picker_macos/README.md b/packages/image_picker/image_picker_macos/README.md new file mode 100644 index 000000000000..ec76d85e26be --- /dev/null +++ b/packages/image_picker/image_picker_macos/README.md @@ -0,0 +1,38 @@ +# image\_picker\_macos + +A macOS implementation of [`image_picker`][1]. + +## Limitations + +`ImageSource.camera` is not supported unless a `cameraDelegate` is set. + +### pickImage() +The arguments `maxWidth`, `maxHeight`, and `imageQuality` are not currently supported. + +### pickVideo() +The argument `maxDuration` is not currently supported. + +## Usage + +### Import the package + +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +### Entitlements + +This package is currently implemented using [`file_selector`][3], so you will +need to add a read-only file acces [entitlement][4]: +```xml + com.apple.security.files.user-selected.read-only + +``` + +[1]: https://pub.dev/packages/image_picker +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[3]: https://pub.dev/packages/file_selector +[4]: https://docs.flutter.dev/platform-integration/macos/building#entitlements-and-the-app-sandbox diff --git a/packages/image_picker/image_picker_macos/example/README.md b/packages/image_picker/image_picker_macos/example/README.md new file mode 100644 index 000000000000..96b8bb17dbff --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/README.md @@ -0,0 +1,9 @@ +# Platform Implementation Test App + +This is a test app for manual testing and automated integration testing +of this platform implementation. It is not intended to demonstrate actual use of +this package, since the intent is that plugin clients use the app-facing +package. + +Unless you are making changes to this implementation package, this example is +very unlikely to be relevant. diff --git a/packages/image_picker/image_picker_macos/example/lib/main.dart b/packages/image_picker/image_picker_macos/example/lib/main.dart new file mode 100644 index 000000000000..9e22c716a2e0 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/lib/main.dart @@ -0,0 +1,422 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + title: 'Image Picker Demo', + home: MyHomePage(title: 'Image Picker Example'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, this.title}); + + final String? title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + List? _imageFileList; + + // This must be called from within a setState() callback + void _setImageFileListFromFile(XFile? value) { + _imageFileList = value == null ? null : [value]; + } + + dynamic _pickImageError; + bool _isVideo = false; + + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; + + final ImagePickerPlatform _picker = ImagePickerPlatform.instance; + final TextEditingController maxWidthController = TextEditingController(); + final TextEditingController maxHeightController = TextEditingController(); + final TextEditingController qualityController = TextEditingController(); + + Future _playVideo(XFile? file) async { + if (file != null && mounted) { + await _disposeVideoController(); + final VideoPlayerController controller = + VideoPlayerController.file(File(file.path)); + _controller = controller; + await controller.setVolume(1.0); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); + setState(() {}); + } + } + + Future _handleMultiImagePicked(BuildContext context) async { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List? pickedFileList = await _picker.getMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + + Future _handleSingleImagePicked( + BuildContext context, ImageSource source) async { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } + + Future _onImageButtonPressed(ImageSource source, + {required BuildContext context, bool isMultiImage = false}) async { + if (_controller != null) { + await _controller!.setVolume(0.0); + } + if (context.mounted) { + if (_isVideo) { + final XFile? file = await _picker.getVideo( + source: source, maxDuration: const Duration(seconds: 10)); + await _playVideo(file); + } else if (isMultiImage) { + await _handleMultiImagePicked(context); + } else { + await _handleSingleImagePicked(context, source); + } + } + } + + @override + void deactivate() { + if (_controller != null) { + _controller!.setVolume(0.0); + _controller!.pause(); + } + super.deactivate(); + } + + @override + void dispose() { + _disposeVideoController(); + maxWidthController.dispose(); + maxHeightController.dispose(); + qualityController.dispose(); + super.dispose(); + } + + Future _disposeVideoController() async { + if (_toBeDisposed != null) { + await _toBeDisposed!.dispose(); + } + _toBeDisposed = _controller; + _controller = null; + } + + Widget _previewVideo() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_controller == null) { + return const Text( + 'You have not yet picked a video', + textAlign: TextAlign.center, + ); + } + return Padding( + padding: const EdgeInsets.all(10.0), + child: AspectRatioVideo(_controller), + ); + } + + Widget _previewImages() { + final Text? retrieveError = _getRetrieveErrorWidget(); + if (retrieveError != null) { + return retrieveError; + } + if (_imageFileList != null) { + return Semantics( + label: 'image_picker_example_picked_images', + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (BuildContext context, int index) { + return Semantics( + label: 'image_picker_example_picked_image', + child: Image.file(File(_imageFileList![index].path)), + ); + }, + itemCount: _imageFileList!.length, + ), + ); + } else if (_pickImageError != null) { + return Text( + 'Pick image error: $_pickImageError', + textAlign: TextAlign.center, + ); + } else { + return const Text( + 'You have not yet picked an image.', + textAlign: TextAlign.center, + ); + } + } + + Widget _handlePreview() { + if (_isVideo) { + return _previewVideo(); + } else { + return _previewImages(); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title!), + ), + body: Center( + child: _handlePreview(), + ), + floatingActionButton: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Semantics( + label: 'image_picker_example_from_gallery', + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'image0', + tooltip: 'Pick Image from gallery', + child: const Icon(Icons.photo), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); + }, + heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), + ), + ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.gallery, context: context); + }, + heroTag: 'video0', + tooltip: 'Pick Video from gallery', + child: const Icon(Icons.video_library), + ), + ), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), + ), + ], + ), + ); + } + + Text? _getRetrieveErrorWidget() { + if (_retrieveDataError != null) { + final Text result = Text(_retrieveDataError!); + _retrieveDataError = null; + return result; + } + return null; + } + + Future _displayPickImageDialog( + BuildContext context, OnPickImageCallback onPick) async { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Add optional parameters'), + content: Column( + children: [ + TextField( + controller: maxWidthController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxWidth if desired'), + ), + TextField( + controller: maxHeightController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration( + hintText: 'Enter maxHeight if desired'), + ), + TextField( + controller: qualityController, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + hintText: 'Enter quality if desired'), + ), + ], + ), + actions: [ + TextButton( + child: const Text('CANCEL'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + TextButton( + child: const Text('PICK'), + onPressed: () { + final double? width = maxWidthController.text.isNotEmpty + ? double.parse(maxWidthController.text) + : null; + final double? height = maxHeightController.text.isNotEmpty + ? double.parse(maxHeightController.text) + : null; + final int? quality = qualityController.text.isNotEmpty + ? int.parse(qualityController.text) + : null; + onPick(width, height, quality); + Navigator.of(context).pop(); + }), + ], + ); + }); + } +} + +typedef OnPickImageCallback = void Function( + double? maxWidth, double? maxHeight, int? quality); + +class AspectRatioVideo extends StatefulWidget { + const AspectRatioVideo(this.controller, {super.key}); + + final VideoPlayerController? controller; + + @override + AspectRatioVideoState createState() => AspectRatioVideoState(); +} + +class AspectRatioVideoState extends State { + VideoPlayerController? get controller => widget.controller; + bool initialized = false; + + void _onVideoControllerUpdate() { + if (!mounted) { + return; + } + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + controller!.addListener(_onVideoControllerUpdate); + } + + @override + void dispose() { + controller!.removeListener(_onVideoControllerUpdate); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (initialized) { + return Center( + child: AspectRatio( + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), + ), + ); + } else { + return Container(); + } + } +} diff --git a/packages/image_picker/image_picker_macos/example/macos/.gitignore b/packages/image_picker/image_picker_macos/example/macos/.gitignore new file mode 100644 index 000000000000..746adbb6b9e1 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Release.xcconfig b/packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/image_picker/image_picker_macos/example/macos/Podfile b/packages/image_picker/image_picker_macos/example/macos/Podfile new file mode 100644 index 000000000000..049abe295427 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..d9333e4704c4 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,573 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..fb7259e17785 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..1d526a16ed0f --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/AppDelegate.swift b/packages/image_picker/image_picker_macos/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..5cec4c48f620 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..a2ec33f19f11 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/image_picker/image_picker_macos/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000000..80e867a4e06b --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..e0c85ac3c1f7 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.imagePickerExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 The Flutter Authors. All rights reserved. diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Debug.xcconfig b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..36b0fd9464f4 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Release.xcconfig b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..dff4f49561c8 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Warnings.xcconfig b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000000..42bcbf4780b1 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/DebugProfile.entitlements b/packages/image_picker/image_picker_macos/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..0ceee8dff196 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-only + + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Info.plist b/packages/image_picker/image_picker_macos/example/macos/Runner/Info.plist new file mode 100644 index 000000000000..4789daa6a443 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/MainFlutterWindow.swift b/packages/image_picker/image_picker_macos/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..32aaeedceb1f --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Release.entitlements b/packages/image_picker/image_picker_macos/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..18aff0ce43c2 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/packages/image_picker/image_picker_macos/example/pubspec.yaml b/packages/image_picker/image_picker_macos/example/pubspec.yaml new file mode 100644 index 000000000000..e76c49286ef6 --- /dev/null +++ b/packages/image_picker/image_picker_macos/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: example +description: Example for image_picker_macos implementation. +publish_to: 'none' +version: 1.0.0 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + image_picker_macos: + # When depending on this package from a real application you should use: + # image_picker_macos: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: .. + image_picker_platform_interface: ^2.7.0 + video_player: ^2.1.4 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart new file mode 100644 index 000000000000..7a7e92737b03 --- /dev/null +++ b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart @@ -0,0 +1,162 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_macos/file_selector_macos.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +/// The macOS implementation of [ImagePickerPlatform]. +/// +/// This class implements the `package:image_picker` functionality for +/// macOS. +class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { + /// Constructs a platform implementation. + ImagePickerMacOS(); + + /// The file selector used to prompt the user to select images or videos. + @visibleForTesting + static FileSelectorPlatform fileSelector = FileSelectorMacOS(); + + /// Registers this class as the default instance of [ImagePickerPlatform]. + static void registerWith() { + ImagePickerPlatform.instance = ImagePickerMacOS(); + } + + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getImageFromSource. + @override + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + final XFile? file = await getImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice); + if (file != null) { + return PickedFile(file.path); + } + return null; + } + + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getVideo. + @override + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final XFile? file = await getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + if (file != null) { + return PickedFile(file.path); + } + return null; + } + + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getImageFromSource. + @override + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + return getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice)); + } + + // [ImagePickerOptions] options are not currently supported. If any + // of its fields are set, they will be silently ignored. + // + // If source is `ImageSource.camera`, a `StateError` will be thrown + // unless a [cameraDelegate] is set. + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + switch (source) { + case ImageSource.camera: + return super.getImageFromSource(source: source); + case ImageSource.gallery: + // TODO(stuartmorgan): Add a native implementation that can use + // PHPickerViewController on macOS 13+, with this as a fallback for + // older OS versions: https://github.com/flutter/flutter/issues/125829. + const XTypeGroup typeGroup = + XTypeGroup(uniformTypeIdentifiers: ['public.image']); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; + } + // Ensure that there's a fallback in case a new source is added. + // ignore: dead_code + throw UnimplementedError('Unknown ImageSource: $source'); + } + + // `preferredCameraDevice` and `maxDuration` arguments are not currently + // supported. If either of these arguments are supplied, they will be silently + // ignored. + // + // If source is `ImageSource.camera`, a `StateError` will be thrown + // unless a [cameraDelegate] is set. + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + switch (source) { + case ImageSource.camera: + return super.getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + case ImageSource.gallery: + const XTypeGroup typeGroup = + XTypeGroup(uniformTypeIdentifiers: ['public.movie']); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; + } + // Ensure that there's a fallback in case a new source is added. + // ignore: dead_code + throw UnimplementedError('Unknown ImageSource: $source'); + } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently + // supported. If any of these arguments are supplied, they will be silently + // ignored. + @override + Future> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + // TODO(stuartmorgan): Add a native implementation that can use + // PHPickerViewController on macOS 13+, with this as a fallback for + // older OS versions: https://github.com/flutter/flutter/issues/125829. + const XTypeGroup typeGroup = + XTypeGroup(uniformTypeIdentifiers: ['public.image']); + final List files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + return files; + } +} diff --git a/packages/image_picker/image_picker_macos/pubspec.yaml b/packages/image_picker/image_picker_macos/pubspec.yaml new file mode 100644 index 000000000000..ef97bd4bc257 --- /dev/null +++ b/packages/image_picker/image_picker_macos/pubspec.yaml @@ -0,0 +1,29 @@ +name: image_picker_macos +description: macOS platform implementation of image_picker +repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_macos +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 +version: 0.2.0 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +flutter: + plugin: + implements: image_picker + platforms: + macos: + dartPluginClass: ImagePickerMacOS + +dependencies: + file_selector_macos: ^0.9.1+1 + file_selector_platform_interface: ^2.3.0 + flutter: + sdk: flutter + image_picker_platform_interface: ^2.7.0 + +dev_dependencies: + build_runner: ^2.1.5 + flutter_test: + sdk: flutter + mockito: 5.4.1 diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart new file mode 100644 index 000000000000..f2b45cf33db9 --- /dev/null +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart @@ -0,0 +1,154 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_macos/image_picker_macos.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'image_picker_macos_test.mocks.dart'; + +@GenerateMocks([FileSelectorPlatform]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Returns the captured type groups from a mock call result, assuming that + // exactly one call was made and only the type groups were captured. + List capturedTypeGroups(VerificationResult result) { + return result.captured.single as List; + } + + late ImagePickerMacOS plugin; + late MockFileSelectorPlatform mockFileSelectorPlatform; + + setUp(() { + plugin = ImagePickerMacOS(); + mockFileSelectorPlatform = MockFileSelectorPlatform(); + + when(mockFileSelectorPlatform.openFile( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .thenAnswer((_) async => null); + + when(mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: anyNamed('acceptedTypeGroups'))) + .thenAnswer((_) async => List.empty()); + + ImagePickerMacOS.fileSelector = mockFileSelectorPlatform; + }); + + test('registered instance', () { + ImagePickerMacOS.registerWith(); + expect(ImagePickerPlatform.instance, isA()); + }); + + group('images', () { + test('pickImage passes the accepted type groups correctly', () async { + await plugin.pickImage(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.image']); + }); + + test('getImage passes the accepted type groups correctly', () async { + await plugin.getImage(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.image']); + }); + + test('getImageFromSource passes the accepted type groups correctly', + () async { + await plugin.getImageFromSource(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.image']); + }); + + test('getImageFromSource calls delegate when source is camera', () async { + const String fakePath = '/tmp/foo'; + plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); + expect( + (await plugin.getImageFromSource(source: ImageSource.camera))!.path, + fakePath); + }); + + test( + 'getImageFromSource throws StateError when source is camera with no delegate', + () async { + await expectLater(plugin.getImageFromSource(source: ImageSource.camera), + throwsStateError); + }); + + test('getMultiImage passes the accepted type groups correctly', () async { + await plugin.getMultiImage(); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.image']); + }); + }); + + group('videos', () { + test('pickVideo passes the accepted type groups correctly', () async { + await plugin.pickVideo(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.movie']); + }); + + test('getVideo passes the accepted type groups correctly', () async { + await plugin.getVideo(source: ImageSource.gallery); + + final VerificationResult result = verify(mockFileSelectorPlatform + .openFile(acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].uniformTypeIdentifiers, + ['public.movie']); + }); + + test('getVideo calls delegate when source is camera', () async { + const String fakePath = '/tmp/foo'; + plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); + expect( + (await plugin.getVideo(source: ImageSource.camera))!.path, fakePath); + }); + + test('getVideo throws StateError when source is camera with no delegate', + () async { + await expectLater( + plugin.getVideo(source: ImageSource.camera), throwsStateError); + }); + }); +} + +class FakeCameraDelegate extends ImagePickerCameraDelegate { + FakeCameraDelegate({this.result}); + + XFile? result; + + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } +} diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart new file mode 100644 index 000000000000..5b8769c27aad --- /dev/null +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.mocks.dart @@ -0,0 +1,120 @@ +// Mocks generated by Mockito 5.4.0 from annotations +// in image_picker_macos/test/image_picker_macos_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart' + as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [FileSelectorPlatform]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockFileSelectorPlatform extends _i1.Mock + implements _i2.FileSelectorPlatform { + MockFileSelectorPlatform() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i2.XFile?> openFile({ + List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #openFile, + [], + { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future<_i2.XFile?>.value(), + ) as _i3.Future<_i2.XFile?>); + @override + _i3.Future> openFiles({ + List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #openFiles, + [], + { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future>.value(<_i2.XFile>[]), + ) as _i3.Future>); + @override + _i3.Future getSavePath({ + List<_i2.XTypeGroup>? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #getSavePath, + [], + { + #acceptedTypeGroups: acceptedTypeGroups, + #initialDirectory: initialDirectory, + #suggestedName: suggestedName, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #getDirectoryPath, + [], + { + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) => + (super.noSuchMethod( + Invocation.method( + #getDirectoryPaths, + [], + { + #initialDirectory: initialDirectory, + #confirmButtonText: confirmButtonText, + }, + ), + returnValue: _i3.Future>.value([]), + ) as _i3.Future>); +} diff --git a/packages/image_picker/image_picker_windows/AUTHORS b/packages/image_picker/image_picker_windows/AUTHORS index 5db3d584e6bc..26e81c7fb254 100644 --- a/packages/image_picker/image_picker_windows/AUTHORS +++ b/packages/image_picker/image_picker_windows/AUTHORS @@ -4,4 +4,4 @@ # Name/Organization Google Inc. -Alexandre Zollinger Chohfi \ No newline at end of file +Alexandre Zollinger Chohfi diff --git a/packages/image_picker/image_picker_windows/CHANGELOG.md b/packages/image_picker/image_picker_windows/CHANGELOG.md index 9c6267cbc0cf..2159d8701228 100644 --- a/packages/image_picker/image_picker_windows/CHANGELOG.md +++ b/packages/image_picker/image_picker_windows/CHANGELOG.md @@ -1,4 +1,4 @@ -## NEXT +## 0.2.0 * Updates minimum Flutter version to 3.3. diff --git a/packages/image_picker/image_picker_windows/README.md b/packages/image_picker/image_picker_windows/README.md index 0336723884ca..1aa30b17fc0b 100644 --- a/packages/image_picker/image_picker_windows/README.md +++ b/packages/image_picker/image_picker_windows/README.md @@ -2,19 +2,26 @@ A Windows implementation of [`image_picker`][1]. +## Limitations + +`ImageSource.camera` is not supported unless a `cameraDelegate` is set. + ### pickImage() -The arguments `source`, `maxWidth`, `maxHeight`, `imageQuality`, and `preferredCameraDevice` are not supported on Windows. +The arguments `maxWidth`, `maxHeight`, and `imageQuality` are not currently supported. ### pickVideo() -The arguments `source`, `preferredCameraDevice`, and `maxDuration` are not supported on Windows. +The argument `maxDuration` is not currently supported. ## Usage ### Import the package -This package is not yet [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), -which means you need to [add `image_picker_windows` as a dependency](https://pub.dev/packages/image_picker_windows/install) -in addition to `image_picker`. +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. -Once you do, you can use the `image_picker` APIs as you normally would, other -than the limitations noted above. +[1]: https://pub.dev/packages/image_picker +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/image_picker/image_picker_windows/example/lib/main.dart b/packages/image_picker/image_picker_windows/example/lib/main.dart index b9ee929977db..9e22c716a2e0 100644 --- a/packages/image_picker/image_picker_windows/example/lib/main.dart +++ b/packages/image_picker/image_picker_windows/example/lib/main.dart @@ -37,11 +37,11 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _imageFileList; // This must be called from within a setState() callback - void _setImageFileListFromFile(PickedFile? value) { - _imageFileList = value == null ? null : [value]; + void _setImageFileListFromFile(XFile? value) { + _imageFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -56,7 +56,7 @@ class _MyHomePageState extends State { final TextEditingController maxHeightController = TextEditingController(); final TextEditingController qualityController = TextEditingController(); - Future _playVideo(PickedFile? file) async { + Future _playVideo(XFile? file) async { if (file != null && mounted) { await _disposeVideoController(); final VideoPlayerController controller = @@ -74,7 +74,7 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { - final List? pickedFileList = await _picker.pickMultiImage( + final List? pickedFileList = await _picker.getMultiImage( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, @@ -95,11 +95,13 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { - final PickedFile? pickedFile = await _picker.pickImage( + final XFile? pickedFile = await _picker.getImageFromSource( source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), ); setState(() { _setImageFileListFromFile(pickedFile); @@ -119,7 +121,7 @@ class _MyHomePageState extends State { } if (context.mounted) { if (_isVideo) { - final PickedFile? file = await _picker.pickVideo( + final XFile? file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else if (isMultiImage) { @@ -253,18 +255,19 @@ class _MyHomePageState extends State { child: const Icon(Icons.photo_library), ), ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: FloatingActionButton( - onPressed: () { - _isVideo = false; - _onImageButtonPressed(ImageSource.camera, context: context); - }, - heroTag: 'image2', - tooltip: 'Take a Photo', - child: const Icon(Icons.camera_alt), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), ), - ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( @@ -278,19 +281,20 @@ class _MyHomePageState extends State { child: const Icon(Icons.video_library), ), ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: FloatingActionButton( - backgroundColor: Colors.red, - onPressed: () { - _isVideo = true; - _onImageButtonPressed(ImageSource.camera, context: context); - }, - heroTag: 'video1', - tooltip: 'Take a Video', - child: const Icon(Icons.videocam), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + _isVideo = true; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), ), - ), ], ), ); diff --git a/packages/image_picker/image_picker_windows/example/pubspec.yaml b/packages/image_picker/image_picker_windows/example/pubspec.yaml index 3f13f771185a..a645670f379d 100644 --- a/packages/image_picker/image_picker_windows/example/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/example/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - image_picker_platform_interface: ^2.4.3 + image_picker_platform_interface: ^2.7.0 image_picker_windows: # When depending on this package from a real application you should use: # image_picker_windows: ^x.y.z diff --git a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart index 90e86bf486b4..ba7ff4d6e70f 100644 --- a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart +++ b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart @@ -13,7 +13,7 @@ import 'package:image_picker_platform_interface/image_picker_platform_interface. /// /// This class implements the `package:image_picker` functionality for /// Windows. -class ImagePickerWindows extends ImagePickerPlatform { +class ImagePickerWindows extends CameraDelegatingImagePickerPlatform { /// Constructs a ImagePickerWindows. ImagePickerWindows(); @@ -53,11 +53,8 @@ class ImagePickerWindows extends ImagePickerPlatform { ImagePickerPlatform.instance = ImagePickerWindows(); } - // `maxWidth`, `maxHeight`, `imageQuality` and `preferredCameraDevice` - // arguments are not supported on Windows. If any of these arguments - // is supplied, it'll be silently ignored by the Windows version of - // the plugin. `source` is not implemented for `ImageSource.camera` - // and will throw an exception. + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getImageFromSource. @override Future pickImage({ required ImageSource source, @@ -66,23 +63,21 @@ class ImagePickerWindows extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - final XFile? file = await getImage( + final XFile? file = await getImageFromSource( source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: imageQuality, - preferredCameraDevice: preferredCameraDevice); + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice)); if (file != null) { return PickedFile(file.path); } return null; } - // `preferredCameraDevice` and `maxDuration` arguments are not - // supported on Windows. If any of these arguments is supplied, - // it'll be silently ignored by the Windows version of the plugin. - // `source` is not implemented for `ImageSource.camera` and will - // throw an exception. + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getVideo. @override Future pickVideo({ required ImageSource source, @@ -99,11 +94,8 @@ class ImagePickerWindows extends ImagePickerPlatform { return null; } - // `maxWidth`, `maxHeight`, `imageQuality`, and `preferredCameraDevice` - // arguments are not supported on Windows. If any of these arguments - // is supplied, it'll be silently ignored by the Windows version - // of the plugin. `source` is not implemented for `ImageSource.camera` - // and will throw an exception. + // This is soft-deprecated in the platform interface, and is only implemented + // for compatibility. Callers should be using getImageFromSource. @override Future getImage({ required ImageSource source, @@ -112,46 +104,73 @@ class ImagePickerWindows extends ImagePickerPlatform { int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - if (source != ImageSource.gallery) { - // TODO(azchohfi): Support ImageSource.camera. - // See https://github.com/flutter/flutter/issues/102115 - throw UnimplementedError( - 'ImageSource.gallery is currently the only supported source on Windows'); + return getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice)); + } + + // [ImagePickerOptions] options are not currently supported. If any + // of its fields are set, they will be silently ignored. + // + // If source is `ImageSource.camera`, a `StateError` will be thrown + // unless a [cameraDelegate] is set. + @override + Future getImageFromSource({ + required ImageSource source, + ImagePickerOptions options = const ImagePickerOptions(), + }) async { + switch (source) { + case ImageSource.camera: + return super.getImageFromSource(source: source); + case ImageSource.gallery: + const XTypeGroup typeGroup = + XTypeGroup(label: 'Images', extensions: imageFormats); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; } - const XTypeGroup typeGroup = - XTypeGroup(label: 'images', extensions: imageFormats); - final XFile? file = await fileSelector - .openFile(acceptedTypeGroups: [typeGroup]); - return file; + // Ensure that there's a fallback in case a new source is added. + // ignore: dead_code + throw UnimplementedError('Unknown ImageSource: $source'); } - // `preferredCameraDevice` and `maxDuration` arguments are not - // supported on Windows. If any of these arguments is supplied, - // it'll be silently ignored by the Windows version of the plugin. - // `source` is not implemented for `ImageSource.camera` and will - // throw an exception. + // `preferredCameraDevice` and `maxDuration` arguments are not currently + // supported. If either of these arguments are supplied, they will be silently + // ignored. + // + // If source is `ImageSource.camera`, a `StateError` will be thrown + // unless a [cameraDelegate] is set. @override Future getVideo({ required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, Duration? maxDuration, }) async { - if (source != ImageSource.gallery) { - // TODO(azchohfi): Support ImageSource.camera. - // See https://github.com/flutter/flutter/issues/102115 - throw UnimplementedError( - 'ImageSource.gallery is currently the only supported source on Windows'); + switch (source) { + case ImageSource.camera: + return super.getVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration); + case ImageSource.gallery: + const XTypeGroup typeGroup = + XTypeGroup(label: 'Videos', extensions: videoFormats); + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + return file; } - const XTypeGroup typeGroup = - XTypeGroup(label: 'videos', extensions: videoFormats); - final XFile? file = await fileSelector - .openFile(acceptedTypeGroups: [typeGroup]); - return file; + // Ensure that there's a fallback in case a new source is added. + // ignore: dead_code + throw UnimplementedError('Unknown ImageSource: $source'); } - // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not - // supported on Windows. If any of these arguments is supplied, - // it'll be silently ignored by the Windows version of the plugin. + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently + // supported. If any of these arguments are supplied, they will be silently + // ignored. @override Future> getMultiImage({ double? maxWidth, @@ -159,7 +178,7 @@ class ImagePickerWindows extends ImagePickerPlatform { int? imageQuality, }) async { const XTypeGroup typeGroup = - XTypeGroup(label: 'images', extensions: imageFormats); + XTypeGroup(label: 'Images', extensions: imageFormats); final List files = await fileSelector .openFiles(acceptedTypeGroups: [typeGroup]); return files; diff --git a/packages/image_picker/image_picker_windows/pubspec.yaml b/packages/image_picker/image_picker_windows/pubspec.yaml index 5984efd3536a..2ca2fc555739 100644 --- a/packages/image_picker/image_picker_windows/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_windows description: Windows platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.1.0+6 +version: 0.2.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -17,10 +17,10 @@ flutter: dependencies: file_selector_platform_interface: ^2.2.0 - file_selector_windows: ^0.8.2 + file_selector_windows: ^0.9.0 flutter: sdk: flutter - image_picker_platform_interface: ^2.4.3 + image_picker_platform_interface: ^2.7.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart index f8adde4051c7..d680d782f6cb 100644 --- a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart +++ b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart @@ -21,11 +21,12 @@ void main() { return result.captured.single as List; } - group('$ImagePickerWindows()', () { - final ImagePickerWindows plugin = ImagePickerWindows(); + group('ImagePickerWindows', () { + late ImagePickerWindows plugin; late MockFileSelectorPlatform mockFileSelectorPlatform; setUp(() { + plugin = ImagePickerWindows(); mockFileSelectorPlatform = MockFileSelectorPlatform(); when(mockFileSelectorPlatform.openFile( @@ -55,12 +56,6 @@ void main() { ImagePickerWindows.imageFormats); }); - test('pickImage throws UnimplementedError when source is camera', - () async { - expect(() async => plugin.pickImage(source: ImageSource.camera), - throwsA(isA())); - }); - test('getImage passes the accepted type groups correctly', () async { await plugin.getImage(source: ImageSource.gallery); @@ -71,10 +66,21 @@ void main() { ImagePickerWindows.imageFormats); }); - test('getImage throws UnimplementedError when source is camera', + test('getMultiImage passes the accepted type groups correctly', () async { + await plugin.getMultiImage(); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, + ImagePickerWindows.imageFormats); + }); + + test( + 'getImageFromSource throws StateError when source is camera with no delegate', () async { - expect(() async => plugin.getImage(source: ImageSource.camera), - throwsA(isA())); + await expectLater(plugin.getImageFromSource(source: ImageSource.camera), + throwsStateError); }); test('getMultiImage passes the accepted type groups correctly', () async { @@ -87,6 +93,7 @@ void main() { ImagePickerWindows.imageFormats); }); }); + group('videos', () { test('pickVideo passes the accepted type groups correctly', () async { await plugin.pickVideo(source: ImageSource.gallery); @@ -98,12 +105,6 @@ void main() { ImagePickerWindows.videoFormats); }); - test('pickVideo throws UnimplementedError when source is camera', - () async { - expect(() async => plugin.pickVideo(source: ImageSource.camera), - throwsA(isA())); - }); - test('getVideo passes the accepted type groups correctly', () async { await plugin.getVideo(source: ImageSource.gallery); @@ -114,11 +115,38 @@ void main() { ImagePickerWindows.videoFormats); }); - test('getVideo throws UnimplementedError when source is camera', + test('getVideo calls delegate when source is camera', () async { + const String fakePath = '/tmp/foo'; + plugin.cameraDelegate = FakeCameraDelegate(result: XFile(fakePath)); + expect((await plugin.getVideo(source: ImageSource.camera))!.path, + fakePath); + }); + + test('getVideo throws StateError when source is camera with no delegate', () async { - expect(() async => plugin.getVideo(source: ImageSource.camera), - throwsA(isA())); + await expectLater( + plugin.getVideo(source: ImageSource.camera), throwsStateError); }); }); }); } + +class FakeCameraDelegate extends ImagePickerCameraDelegate { + FakeCameraDelegate({this.result}); + + XFile? result; + + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return result; + } +} diff --git a/script/configs/exclude_integration_linux.yaml b/script/configs/exclude_integration_linux.yaml index a83550e6808f..1f5ad8c843bc 100644 --- a/script/configs/exclude_integration_linux.yaml +++ b/script/configs/exclude_integration_linux.yaml @@ -1,3 +1,4 @@ # Can't use Flutter integration tests due to native modal UI. - file_selector - file_selector_linux +- image_picker_linux diff --git a/script/configs/exclude_integration_macos.yaml b/script/configs/exclude_integration_macos.yaml index 7a9e287da05f..e2b639cb96f9 100644 --- a/script/configs/exclude_integration_macos.yaml +++ b/script/configs/exclude_integration_macos.yaml @@ -1,3 +1,4 @@ # Can't use Flutter integration tests due to native modal UI. - file_selector - file_selector_macos +- image_picker_macos From 6565f17bcd1b116b1141e76893b5f95959178e61 Mon Sep 17 00:00:00 2001 From: Tarrin Neal Date: Fri, 9 Jun 2023 16:15:51 -0700 Subject: [PATCH 14/53] [image_picker] getMedia platform changes (#4174) Adds `getMedia` and `getMultipleMedia` methods to image_picker_platform_interface. precursor to https://github.com/flutter/packages/pull/3892 part of https://github.com/flutter/flutter/issues/89159 --- .../CHANGELOG.md | 4 + .../method_channel_image_picker.dart | 42 ++++- .../image_picker_platform.dart | 18 +++ .../lib/src/types/image_options.dart | 61 ++++++++ .../lib/src/types/image_picker_options.dart | 50 ------ .../lib/src/types/lost_data_response.dart | 5 +- .../lib/src/types/media_options.dart | 23 +++ .../lib/src/types/media_selection_type.dart | 14 ++ .../lib/src/types/retrieve_type.dart | 5 +- .../lib/src/types/types.dart | 6 +- .../pubspec.yaml | 2 +- .../method_channel_image_picker_test.dart | 146 ++++++++++++++++++ 12 files changed, 316 insertions(+), 60 deletions(-) delete mode 100644 packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart create mode 100644 packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart create mode 100644 packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index f93b6ec181ec..bcba89bd3974 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.8.0 + +* Adds `getMedia` method. + ## 2.7.0 * Adds `CameraDelegatingImagePickerPlatform` as a base class for platform diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart index c2c39f93fe18..b21fd29a8d2d 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart @@ -252,6 +252,30 @@ class MethodChannelImagePicker extends ImagePickerPlatform { return paths.map((dynamic path) => XFile(path as String)).toList(); } + @override + Future> getMedia({ + required MediaOptions options, + }) async { + final ImageOptions imageOptions = options.imageOptions; + + final Map args = { + 'maxImageWidth': imageOptions.maxWidth, + 'maxImageHeight': imageOptions.maxHeight, + 'imageQuality': imageOptions.imageQuality, + 'allowMultiple': options.allowMultiple, + }; + + final List? paths = await _channel + .invokeMethod?>( + 'pickMedia', + args, + ) + .then((List? paths) => + paths?.map((dynamic path) => XFile(path as String)).toList()); + + return paths ?? []; + } + @override Future getVideo({ required ImageSource source, @@ -280,13 +304,21 @@ class MethodChannelImagePicker extends ImagePickerPlatform { assert(result.containsKey('path') != result.containsKey('errorCode')); final String? type = result['type'] as String?; - assert(type == kTypeImage || type == kTypeVideo); + assert( + type == kTypeImage || type == kTypeVideo || type == kTypeMedia, + ); RetrieveType? retrieveType; - if (type == kTypeImage) { - retrieveType = RetrieveType.image; - } else if (type == kTypeVideo) { - retrieveType = RetrieveType.video; + switch (type) { + case kTypeImage: + retrieveType = RetrieveType.image; + break; + case kTypeVideo: + retrieveType = RetrieveType.video; + break; + case kTypeMedia: + retrieveType = RetrieveType.media; + break; } PlatformException? exception; diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart index e01caca14616..66c5d3b57854 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart @@ -213,6 +213,24 @@ abstract class ImagePickerPlatform extends PlatformInterface { throw UnimplementedError('getMultiImage() has not been implemented.'); } + /// Returns a [List] with the images and/or videos that were picked. + /// The images and videos come from the gallery. + /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and + /// above only support HEIC images if used in addition to a size modification, + /// of which the usage is explained below. + /// + /// In Android, the MainActivity can be destroyed for various reasons. + /// If that happens, the result will be lost in this call. You can then + /// call [getLostData] when your app relaunches to retrieve the lost data. + /// + /// If no images or videos were picked, the return value is an empty list. + Future> getMedia({ + required MediaOptions options, + }) { + throw UnimplementedError('getMedia() has not been implemented.'); + } + /// Returns a [XFile] containing the video that was picked. /// /// The [source] argument controls where the video comes from. This can diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart index 2cc01c92da1d..374ff27063fa 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_options.dart @@ -2,6 +2,40 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'types.dart'; + +/// Specifies options for picking a single image from the device's camera or gallery. +/// +/// This class inheritance is a byproduct of the api changing over time. +/// It exists solely to avoid breaking changes. +class ImagePickerOptions extends ImageOptions { + /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality], + /// [referredCameraDevice] and [requestFullMetadata]. + const ImagePickerOptions({ + super.maxHeight, + super.maxWidth, + super.imageQuality, + super.requestFullMetadata, + this.preferredCameraDevice = CameraDevice.rear, + }) : super(); + + /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality], + /// [referredCameraDevice] and [requestFullMetadata]. + ImagePickerOptions.createAndValidate({ + super.maxHeight, + super.maxWidth, + super.imageQuality, + super.requestFullMetadata, + this.preferredCameraDevice = CameraDevice.rear, + }) : super.createAndValidate(); + + /// Used to specify the camera to use when the `source` is [ImageSource.camera]. + /// + /// Ignored if the source is not [ImageSource.camera], or the chosen camera is not + /// supported on the device. Defaults to [CameraDevice.rear]. + final CameraDevice preferredCameraDevice; +} + /// Specifies image-specific options for picking. class ImageOptions { /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality] @@ -13,6 +47,18 @@ class ImageOptions { this.requestFullMetadata = true, }); + /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality] + /// and [requestFullMetadata]. Throws if options are not valid. + ImageOptions.createAndValidate({ + this.maxHeight, + this.maxWidth, + this.imageQuality, + this.requestFullMetadata = true, + }) { + _validateOptions( + maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality); + } + /// The maximum width of the image, in pixels. /// /// If null, the image will only be resized if [maxHeight] is specified. @@ -38,4 +84,19 @@ class ImageOptions { // // Defaults to true. final bool requestFullMetadata; + + /// Validates that all values are within required ranges. Throws if not. + static void _validateOptions( + {double? maxWidth, final double? maxHeight, int? imageQuality}) { + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart deleted file mode 100644 index 0d85c918f649..000000000000 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_picker_options.dart +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'types.dart'; - -/// Specifies options for picking a single image from the device's camera or gallery. -class ImagePickerOptions { - /// Creates an instance with the given [maxHeight], [maxWidth], [imageQuality], - /// [referredCameraDevice] and [requestFullMetadata]. - const ImagePickerOptions({ - this.maxHeight, - this.maxWidth, - this.imageQuality, - this.preferredCameraDevice = CameraDevice.rear, - this.requestFullMetadata = true, - }); - - /// The maximum width of the image, in pixels. - /// - /// If null, the image will only be resized if [maxHeight] is specified. - final double? maxWidth; - - /// The maximum height of the image, in pixels. - /// - /// If null, the image will only be resized if [maxWidth] is specified. - final double? maxHeight; - - /// Modifies the quality of the image, ranging from 0-100 where 100 is the - /// original/max quality. - /// - /// Compression is only supported for certain image types such as JPEG. If - /// compression is not supported for the image that is picked, a warning - /// message will be logged. - /// - /// If null, the image will be returned with the original quality. - final int? imageQuality; - - /// Used to specify the camera to use when the `source` is [ImageSource.camera]. - /// - /// Ignored if the source is not [ImageSource.camera], or the chosen camera is not - /// supported on the device. Defaults to [CameraDevice.rear]. - final CameraDevice preferredCameraDevice; - - /// If true, requests full image metadata, which may require extra permissions - /// on some platforms, (e.g., NSPhotoLibraryUsageDescription on iOS). - // - // Defaults to true. - final bool requestFullMetadata; -} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart index 10af812a3109..0f802f19719f 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart @@ -36,7 +36,8 @@ class LostDataResponse { /// An empty response should have [file], [exception] and [type] to be null. bool get isEmpty => _empty; - /// The file that was lost in a previous [getImage], [getMultiImage] or [getVideo] call due to MainActivity being destroyed. + /// The file that was lost in a previous [getImage], [getMultiImage], + /// [getVideo] or [getMedia] call due to MainActivity being destroyed. /// /// Can be null if [exception] exists. final XFile? file; @@ -51,7 +52,7 @@ class LostDataResponse { /// Note that it is not the exception that caused the destruction of the MainActivity. final PlatformException? exception; - /// Can either be [RetrieveType.image] or [RetrieveType.video]; + /// Can either be [RetrieveType.image], [RetrieveType.video], or [RetrieveType.media]. /// /// If the lost data is empty, this will be null. final RetrieveType? type; diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart new file mode 100644 index 000000000000..70a048f7147d --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_options.dart @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; + +import '../../image_picker_platform_interface.dart'; + +/// Specifies options for selecting items when using [ImagePickerPlatform.getMedia]. +@immutable +class MediaOptions { + /// Construct a new MediaOptions instance. + const MediaOptions({ + this.imageOptions = const ImageOptions(), + required this.allowMultiple, + }); + + /// Options that will apply to images upon selection. + final ImageOptions imageOptions; + + /// Whether to allow for selecting multiple media. + final bool allowMultiple; +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart new file mode 100644 index 000000000000..cd0113497ea1 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/media_selection_type.dart @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../image_picker_platform_interface.dart'; + +/// The type of media to allow the user to select with [ImagePickerPlatform.getMedia]. +enum MediaSelectionType { + /// Static pictures. + image, + + /// Videos. + video, +} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart index 445445e5d7fb..94fed59f238d 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart @@ -8,5 +8,8 @@ enum RetrieveType { image, /// A video. See [ImagePicker.pickVideo]. - video + video, + + /// Either a video or a static picture. See [ImagePicker.pickMedia]. + media, } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart index fcb76ccefa2f..0339d98b575e 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart @@ -5,9 +5,10 @@ export 'camera_delegate.dart'; export 'camera_device.dart'; export 'image_options.dart'; -export 'image_picker_options.dart'; export 'image_source.dart'; export 'lost_data_response.dart'; +export 'media_options.dart'; +export 'media_selection_type.dart'; export 'multi_image_picker_options.dart'; export 'picked_file/picked_file.dart'; export 'retrieve_type.dart'; @@ -17,3 +18,6 @@ const String kTypeImage = 'image'; /// Denotes that a video is being picked. const String kTypeVideo = 'video'; + +/// Denotes that either a video or image is being picked. +const String kTypeMedia = 'media'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 3f1e523453f8..67a5070f6c78 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/image_picker/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.7.0 +version: 2.8.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart index 244af3982672..cf92c2cfa145 100644 --- a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart @@ -872,6 +872,152 @@ void main() { }); }); + group('#getMedia', () { + test('calls the method correctly', () async { + returnValue = ['0']; + await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + + expect( + log, + [ + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': null, + 'imageQuality': null, + 'allowMultiple': true, + }), + ], + ); + }); + + test('passes the selection options correctly', () async { + // Default options + returnValue = ['0']; + await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + // Various image options + returnValue = ['0']; + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + ), + ), + ); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxHeight: 10.0, + ), + ), + ); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + imageQuality: 70, + ), + ), + ); + + expect( + log, + [ + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': null, + 'imageQuality': null, + 'allowMultiple': true, + }), + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': 10.0, + 'maxImageHeight': null, + 'imageQuality': null, + 'allowMultiple': true, + }), + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': 10.0, + 'imageQuality': null, + 'allowMultiple': true, + }), + isMethodCall('pickMedia', arguments: { + 'maxImageWidth': null, + 'maxImageHeight': null, + 'imageQuality': 70, + 'allowMultiple': true, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: -1.0, + ), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxHeight: -1.0, + ), + ), + ), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + imageQuality: -1, + ), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + imageQuality: 101, + ), + ), + ), + throwsArgumentError, + ); + }); + + test('handles a null path response gracefully', () async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); + expect( + await picker.getMedia( + options: const MediaOptions(allowMultiple: true)), + []); + }); + }); + group('#getVideo', () { test('passes the image source argument correctly', () async { await picker.getVideo(source: ImageSource.camera); From cd7b93532e5cb605a42735e20f1de70fc00adae7 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 9 Jun 2023 20:27:10 -0400 Subject: [PATCH 15/53] [image_picker] Add desktop support (#3882) (This is the combination PR for overall review.) This adds initial desktop support across all three desktop platforms: - Adds the concept of a camera delegation as discussed in https://github.com/flutter/flutter/issues/102115, and adds a helper base class at the platform interface level for implementations that want to use that approach. (Sharing it simplifies both the platform implementations and the client usage.) - Adds a new app-facing and platform-interface API to check at runtime for support for a given source, to make writing code using `image_picker` in a world of dynamic camera support viable. (In particular this is critical for published packages that use `image_picker`, as they won't know in advance if a client app will have a delegate set up). - Updates the Windows implementation to: - use the new delegation base class - implement the newer `getImageFromSource` API as an opportunistic fix while changing the code (if not implemented, it would fall back to `getImage`) - use new APIs in its example. - Made macOS and Linux implementations that are clones of the Windows approach. They are both slightly simpler however, as MIME type (for Linux) and UTI (for macOS) allow creating generic "image" and "video" filters instead of having to hard-code a file extension list. Since in my opinion this level of support is sufficient to allow basic real-world use of the desktop implementations, due to having a structure in place for handling camera support, this includes endorsing the desktop implementations. It is still the case that desktop does not support image resizing, which isn't ideal, but I don't think we should block on that. It is clearly documented in the README. The desktop implementations are separate packages even though they are mostly the same right now because: - the amount of (non-example) code being duplicated is small, and - it makes it much easier for us to change platform implementations over time; e.g., recent versions of macOS support `PHPicker`, meaning we can later add native code for that to the macOS implementation, with just a fallback to the current `file_selector` wrapper. It's plausible that better native options for Windows and/or Linux may become available in the future as well. Fixes https://github.com/flutter/flutter/issues/102115 Fixes https://github.com/flutter/flutter/issues/102320 Fixes https://github.com/flutter/flutter/issues/85100 --- CODEOWNERS | 2 + .../image_picker/image_picker/CHANGELOG.md | 7 + packages/image_picker/image_picker/README.md | 61 +- .../image_picker/example/lib/main.dart | 48 +- .../example/lib/readme_excerpts.dart | 36 ++ .../image_picker/example/linux/.gitignore | 1 + .../image_picker/example/linux/CMakeLists.txt | 138 +++++ .../example/linux/flutter/CMakeLists.txt | 88 +++ .../linux/flutter/generated_plugins.cmake | 24 + .../image_picker/example/linux/main.cc | 10 + .../example/linux/my_application.cc | 111 ++++ .../example/linux/my_application.h | 22 + .../image_picker/example/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../image_picker/example/macos/Podfile | 40 ++ .../macos/Runner.xcodeproj/project.pbxproj | 573 ++++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 +++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../example/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 +++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 +++++++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + .../example/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 19 + .../example/macos/Runner/Release.entitlements | 8 + .../image_picker/example/pubspec.yaml | 2 +- .../image_picker/example/windows/.gitignore | 17 + .../example/windows/CMakeLists.txt | 101 +++ .../example/windows/flutter/CMakeLists.txt | 104 ++++ .../windows/flutter/generated_plugins.cmake | 24 + .../example/windows/runner/CMakeLists.txt | 40 ++ .../example/windows/runner/Runner.rc | 121 ++++ .../example/windows/runner/flutter_window.cpp | 68 +++ .../example/windows/runner/flutter_window.h | 37 ++ .../example/windows/runner/main.cpp | 46 ++ .../example/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 20 + .../example/windows/runner/utils.cpp | 67 ++ .../example/windows/runner/utils.h | 23 + .../example/windows/runner/win32_window.cpp | 284 +++++++++ .../example/windows/runner/win32_window.h | 104 ++++ .../image_picker/lib/image_picker.dart | 8 + .../image_picker/image_picker/pubspec.yaml | 13 +- .../image_picker/test/image_picker_test.dart | 10 + .../test/image_picker_test.mocks.dart | 8 + .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes 67 files changed, 2892 insertions(+), 29 deletions(-) create mode 100644 packages/image_picker/image_picker/example/linux/.gitignore create mode 100644 packages/image_picker/image_picker/example/linux/CMakeLists.txt create mode 100644 packages/image_picker/image_picker/example/linux/flutter/CMakeLists.txt create mode 100644 packages/image_picker/image_picker/example/linux/flutter/generated_plugins.cmake create mode 100644 packages/image_picker/image_picker/example/linux/main.cc create mode 100644 packages/image_picker/image_picker/example/linux/my_application.cc create mode 100644 packages/image_picker/image_picker/example/linux/my_application.h create mode 100644 packages/image_picker/image_picker/example/macos/.gitignore create mode 100644 packages/image_picker/image_picker/example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 packages/image_picker/image_picker/example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 packages/image_picker/image_picker/example/macos/Podfile create mode 100644 packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/image_picker/image_picker/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packages/image_picker/image_picker/example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packages/image_picker/image_picker/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packages/image_picker/image_picker/example/macos/Runner/AppDelegate.swift create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Configs/Debug.xcconfig create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Configs/Release.xcconfig create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 packages/image_picker/image_picker/example/macos/Runner/DebugProfile.entitlements create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Info.plist create mode 100644 packages/image_picker/image_picker/example/macos/Runner/MainFlutterWindow.swift create mode 100644 packages/image_picker/image_picker/example/macos/Runner/Release.entitlements create mode 100644 packages/image_picker/image_picker/example/windows/.gitignore create mode 100644 packages/image_picker/image_picker/example/windows/CMakeLists.txt create mode 100644 packages/image_picker/image_picker/example/windows/flutter/CMakeLists.txt create mode 100644 packages/image_picker/image_picker/example/windows/flutter/generated_plugins.cmake create mode 100644 packages/image_picker/image_picker/example/windows/runner/CMakeLists.txt create mode 100644 packages/image_picker/image_picker/example/windows/runner/Runner.rc create mode 100644 packages/image_picker/image_picker/example/windows/runner/flutter_window.cpp create mode 100644 packages/image_picker/image_picker/example/windows/runner/flutter_window.h create mode 100644 packages/image_picker/image_picker/example/windows/runner/main.cpp create mode 100644 packages/image_picker/image_picker/example/windows/runner/resource.h create mode 100644 packages/image_picker/image_picker/example/windows/runner/resources/app_icon.ico create mode 100644 packages/image_picker/image_picker/example/windows/runner/runner.exe.manifest create mode 100644 packages/image_picker/image_picker/example/windows/runner/utils.cpp create mode 100644 packages/image_picker/image_picker/example/windows/runner/utils.h create mode 100644 packages/image_picker/image_picker/example/windows/runner/win32_window.cpp create mode 100644 packages/image_picker/image_picker/example/windows/runner/win32_window.h create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/CODEOWNERS b/CODEOWNERS index e81c2fb6ae84..dfc8a29c749d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -83,12 +83,14 @@ packages/webview_flutter/webview_flutter_wkwebview/** @cyanglaz # - Linux packages/file_selector/file_selector_linux/** @cbracken +packages/image_picker/image_picker_linux/** @cbracken packages/path_provider/path_provider_linux/** @cbracken packages/shared_preferences/shared_preferences_linux/** @cbracken packages/url_launcher/url_launcher_linux/** @cbracken # - macOS packages/file_selector/file_selector_macos/** @cbracken +packages/image_picker/image_picker_macos/** @cbracken packages/url_launcher/url_launcher_macos/** @cbracken # - Windows diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index f1e09f50b51b..4b9169499758 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.8.8 + +* Adds initial support for Windows, macOS, and Linux. + * See README for current desktop limitations. +* Adds `supportsImageSource` to allow runtime checks for whether a given source + is supported by the current platform's implementation. + ## 0.8.7+5 * Fixes `BuildContext` handling in example. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index b566116c8595..2c5aa5c3f1a2 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -6,9 +6,9 @@ A Flutter plugin for iOS and Android for picking images from the image library, and taking new pictures with the camera. -| | Android | iOS | Web | -|-------------|---------|---------|---------------------------------| -| **Support** | SDK 21+ | iOS 11+ | [See `image_picker_for_web`][1] | +| | Android | iOS | Linux | macOS | Web | Windows | +|-------------|---------|---------|-------|--------|---------------------------------|-------------| +| **Support** | SDK 21+ | iOS 11+ | Any | 10.14+ | [See `image_picker_for_web`][1] | Windows 10+ | ## Installation @@ -109,6 +109,61 @@ As activities cannot communicate between tasks, the image picker activity cannot send back its eventual result to the calling activity. To work around this problem, consider using `launchMode: singleTask` instead. +### Windows, macOS, and Linux + +This plugin currently has limited support for the three desktop platforms, +serving as a wrapper around the [`file_selector`](https://pub.dev/packages/file_selector) +plugin with appropriate file type filters set. Selection modification options, +such as max width and height, are not yet supported. + +By default, `ImageSource.camera` is not supported, since unlike on Android and +iOS there is no system-provided UI for taking photos. However, the desktop +implementations allow delegating to a camera handler by setting a +`cameraDelegate` before using `image_picker`, such as in `main()`: + + +``` dart +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +// ··· +class MyCameraDelegate extends ImagePickerCameraDelegate { + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return _takeAPhoto(options.preferredCameraDevice); + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return _takeAVideo(options.preferredCameraDevice); + } +} +// ··· +void setUpCameraDelegate() { + final ImagePickerPlatform instance = ImagePickerPlatform.instance; + if (instance is CameraDelegatingImagePickerPlatform) { + instance.cameraDelegate = MyCameraDelegate(); + } +} +``` + +Once you have set a `cameraDelegate`, `image_picker` calls with +`ImageSource.camera` will work as normal, calling your provided delegate. We +encourage the community to build packages that implement +`ImagePickerCameraDelegate`, to provide options for desktop camera UI. + +#### macOS installation + +Since the macOS implementation uses `file_selector`, you will need to +add a filesystem access +[entitlement][https://docs.flutter.dev/platform-integration/macos/building#entitlements-and-the-app-sandbox]: +```xml + com.apple.security.files.user-selected.read-only + +``` + ### Example diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 4a96ce194ef9..e7c5dae28514 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -316,18 +316,19 @@ class _MyHomePageState extends State { child: const Icon(Icons.photo_library), ), ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: FloatingActionButton( - onPressed: () { - isVideo = false; - _onImageButtonPressed(ImageSource.camera, context: context); - }, - heroTag: 'image2', - tooltip: 'Take a Photo', - child: const Icon(Icons.camera_alt), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', + tooltip: 'Take a Photo', + child: const Icon(Icons.camera_alt), + ), ), - ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( @@ -341,19 +342,20 @@ class _MyHomePageState extends State { child: const Icon(Icons.video_library), ), ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: FloatingActionButton( - backgroundColor: Colors.red, - onPressed: () { - isVideo = true; - _onImageButtonPressed(ImageSource.camera, context: context); - }, - heroTag: 'video1', - tooltip: 'Take a Video', - child: const Icon(Icons.videocam), + if (_picker.supportsImageSource(ImageSource.camera)) + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + onPressed: () { + isVideo = true; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'video1', + tooltip: 'Take a Video', + child: const Icon(Icons.videocam), + ), ), - ), ], ), ); diff --git a/packages/image_picker/image_picker/example/lib/readme_excerpts.dart b/packages/image_picker/image_picker/example/lib/readme_excerpts.dart index 3f0f0788ae3c..e32f4fc84154 100644 --- a/packages/image_picker/image_picker/example/lib/readme_excerpts.dart +++ b/packages/image_picker/image_picker/example/lib/readme_excerpts.dart @@ -4,6 +4,28 @@ import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; +// #docregion CameraDelegate +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +// #enddocregion CameraDelegate + +/// Example of a camera delegate +// #docregion CameraDelegate +class MyCameraDelegate extends ImagePickerCameraDelegate { + @override + Future takePhoto( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return _takeAPhoto(options.preferredCameraDevice); + } + + @override + Future takeVideo( + {ImagePickerCameraDelegateOptions options = + const ImagePickerCameraDelegateOptions()}) async { + return _takeAVideo(options.preferredCameraDevice); + } +} +// #enddocregion CameraDelegate /// Example function for README demonstration of various pick* calls. Future> readmePickExample() async { @@ -49,6 +71,20 @@ Future getLostData() async { } // #enddocregion LostData +/// Example of camera delegate setup. +// #docregion CameraDelegate +void setUpCameraDelegate() { + final ImagePickerPlatform instance = ImagePickerPlatform.instance; + if (instance is CameraDelegatingImagePickerPlatform) { + instance.cameraDelegate = MyCameraDelegate(); + } +} +// #enddocregion CameraDelegate + // Stubs for the getLostData function. void _handleLostFiles(List file) {} void _handleError(PlatformException? exception) {} + +// Stubs for MyCameraDelegate. +Future _takeAPhoto(CameraDevice device) async => null; +Future _takeAVideo(CameraDevice device) async => null; diff --git a/packages/image_picker/image_picker/example/linux/.gitignore b/packages/image_picker/image_picker/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/image_picker/image_picker/example/linux/CMakeLists.txt b/packages/image_picker/image_picker/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..f30eeac66816 --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/CMakeLists.txt @@ -0,0 +1,138 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "image_picker_example") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "dev.flutter.plugins.image_picker_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/image_picker/image_picker/example/linux/flutter/CMakeLists.txt b/packages/image_picker/image_picker/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..d5bd01648a96 --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/image_picker/image_picker/example/linux/flutter/generated_plugins.cmake b/packages/image_picker/image_picker/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..2db3c22ae228 --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/image_picker/image_picker/example/linux/main.cc b/packages/image_picker/image_picker/example/linux/main.cc new file mode 100644 index 000000000000..1507d02825e7 --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/main.cc @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/image_picker/image_picker/example/linux/my_application.cc b/packages/image_picker/image_picker/example/linux/my_application.cc new file mode 100644 index 000000000000..08e3dc4c1603 --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/my_application.cc @@ -0,0 +1,111 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "image_picker_example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "image_picker_example"); + } + + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/packages/image_picker/image_picker/example/linux/my_application.h b/packages/image_picker/image_picker/example/linux/my_application.h new file mode 100644 index 000000000000..6e9f0c3ff665 --- /dev/null +++ b/packages/image_picker/image_picker/example/linux/my_application.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/image_picker/image_picker/example/macos/.gitignore b/packages/image_picker/image_picker/example/macos/.gitignore new file mode 100644 index 000000000000..746adbb6b9e1 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/image_picker/image_picker/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/image_picker/image_picker/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000000..4b81f9b2d200 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/image_picker/image_picker/example/macos/Flutter/Flutter-Release.xcconfig b/packages/image_picker/image_picker/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000000..5caa9d1579e4 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/image_picker/image_picker/example/macos/Podfile b/packages/image_picker/image_picker/example/macos/Podfile new file mode 100644 index 000000000000..049abe295427 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..31d4a2f7511c --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,573 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* image_picker_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "image_picker_example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* image_picker_example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* image_picker_example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.14; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..94322033850a --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/image_picker/image_picker/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..1d526a16ed0f --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/image_picker/image_picker/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/image_picker/image_picker/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/image_picker/image_picker/example/macos/Runner/AppDelegate.swift b/packages/image_picker/image_picker/example/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000000..5cec4c48f620 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..a2ec33f19f11 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/image_picker/image_picker/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/image_picker/image_picker/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000000..d2192aa2260c --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = image_picker_example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.imagePickerExample + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2023 dev.flutter.plugins. All rights reserved. diff --git a/packages/image_picker/image_picker/example/macos/Runner/Configs/Debug.xcconfig b/packages/image_picker/image_picker/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000000..36b0fd9464f4 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/image_picker/image_picker/example/macos/Runner/Configs/Release.xcconfig b/packages/image_picker/image_picker/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000000..dff4f49561c8 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/image_picker/image_picker/example/macos/Runner/Configs/Warnings.xcconfig b/packages/image_picker/image_picker/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000000..42bcbf4780b1 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/packages/image_picker/image_picker/example/macos/Runner/DebugProfile.entitlements b/packages/image_picker/image_picker/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000000..dddb8a30c851 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/packages/image_picker/image_picker/example/macos/Runner/Info.plist b/packages/image_picker/image_picker/example/macos/Runner/Info.plist new file mode 100644 index 000000000000..4789daa6a443 --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/image_picker/image_picker/example/macos/Runner/MainFlutterWindow.swift b/packages/image_picker/image_picker/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000000..32aaeedceb1f --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/packages/image_picker/image_picker/example/macos/Runner/Release.entitlements b/packages/image_picker/image_picker/example/macos/Runner/Release.entitlements new file mode 100644 index 000000000000..852fa1a4728a --- /dev/null +++ b/packages/image_picker/image_picker/example/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index 6b38b56bbed8..fc28de420110 100644 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - image_picker_platform_interface: ^2.6.1 + image_picker_platform_interface: ^2.7.0 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker/example/windows/.gitignore b/packages/image_picker/image_picker/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/image_picker/image_picker/example/windows/CMakeLists.txt b/packages/image_picker/image_picker/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..948306ac83f7 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(image_picker_example LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "image_picker_example") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/packages/image_picker/image_picker/example/windows/flutter/CMakeLists.txt b/packages/image_picker/image_picker/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..930d2071a324 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/packages/image_picker/image_picker/example/windows/flutter/generated_plugins.cmake b/packages/image_picker/image_picker/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..a423a02476a2 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,24 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/image_picker/image_picker/example/windows/runner/CMakeLists.txt b/packages/image_picker/image_picker/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..394917c053a0 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/packages/image_picker/image_picker/example/windows/runner/Runner.rc b/packages/image_picker/image_picker/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..c9d919d0ddc3 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "dev.flutter.plugins" "\0" + VALUE "FileDescription", "image_picker_example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "image_picker_example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2023 dev.flutter.plugins. All rights reserved." "\0" + VALUE "OriginalFilename", "image_picker_example.exe" "\0" + VALUE "ProductName", "image_picker_example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/packages/image_picker/image_picker/example/windows/runner/flutter_window.cpp b/packages/image_picker/image_picker/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..7be2fe290105 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/flutter_window.cpp @@ -0,0 +1,68 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/image_picker/image_picker/example/windows/runner/flutter_window.h b/packages/image_picker/image_picker/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..f1fc669093d0 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/flutter_window.h @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/packages/image_picker/image_picker/example/windows/runner/main.cpp b/packages/image_picker/image_picker/example/windows/runner/main.cpp new file mode 100644 index 000000000000..0428c41b79eb --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/main.cpp @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t* command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"image_picker_example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/image_picker/image_picker/example/windows/runner/resource.h b/packages/image_picker/image_picker/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/image_picker/image_picker/example/windows/runner/resources/app_icon.ico b/packages/image_picker/image_picker/example/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker/example/windows/runner/runner.exe.manifest b/packages/image_picker/image_picker/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..a42ea7687cb6 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker/example/windows/runner/utils.cpp b/packages/image_picker/image_picker/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..cb6aebe4e593 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/utils.cpp @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE* unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = + ::WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, + nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/packages/image_picker/image_picker/example/windows/runner/utils.h b/packages/image_picker/image_picker/example/windows/runner/utils.h new file mode 100644 index 000000000000..bd81e1e02338 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/utils.h @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/image_picker/image_picker/example/windows/runner/win32_window.cpp b/packages/image_picker/image_picker/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..daefc277e4c7 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/win32_window.cpp @@ -0,0 +1,284 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: +/// https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = + L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { ++g_active_window_count; } + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { return ShowWindow(window_handle_, SW_SHOWNORMAL); } + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { return window_handle_; } + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = + RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, RRF_RT_REG_DWORD, nullptr, + &light_mode, &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/packages/image_picker/image_picker/example/windows/runner/win32_window.h b/packages/image_picker/image_picker/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..33d491ead922 --- /dev/null +++ b/packages/image_picker/image_picker/example/windows/runner/win32_window.h @@ -0,0 +1,104 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index ce16763ec6af..0eb35b4bce99 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -351,4 +351,12 @@ class ImagePicker { Future retrieveLostData() { return platform.getLostData(); } + + /// Returns true if the current platform implementation supports [source]. + /// + /// Calling a `pick*` method with a source for which this method + /// returns `false` will throw an error. + bool supportsImageSource(ImageSource source) { + return platform.supportsImageSource(source); + } } diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index f927befc90f1..8b38ba56cae9 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.7+5 +version: 0.8.8 environment: sdk: ">=2.18.0 <4.0.0" @@ -16,8 +16,14 @@ flutter: default_package: image_picker_android ios: default_package: image_picker_ios + linux: + default_package: image_picker_linux + macos: + default_package: image_picker_macos web: default_package: image_picker_for_web + windows: + default_package: image_picker_windows dependencies: flutter: @@ -25,7 +31,10 @@ dependencies: image_picker_android: ^0.8.4+11 image_picker_for_web: ^2.1.0 image_picker_ios: ^0.8.6+1 - image_picker_platform_interface: ^2.6.1 + image_picker_linux: ^0.2.0 + image_picker_macos: ^0.2.0 + image_picker_platform_interface: ^2.7.0 + image_picker_windows: ^0.2.0 dev_dependencies: build_runner: ^2.1.10 diff --git a/packages/image_picker/image_picker/test/image_picker_test.dart b/packages/image_picker/image_picker/test/image_picker_test.dart index 637ecf9c6e7a..459a383b5d97 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -587,5 +587,15 @@ void main() { }); }); }); + + test('supportsImageSource calls through to platform', () async { + final ImagePicker picker = ImagePicker(); + when(mockPlatform.supportsImageSource(any)).thenReturn(true); + + final bool supported = picker.supportsImageSource(ImageSource.camera); + + expect(supported, true); + verify(mockPlatform.supportsImageSource(ImageSource.camera)); + }); }); } diff --git a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart index d1e19830e919..7336d7d50274 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart @@ -225,4 +225,12 @@ class MockImagePickerPlatform extends _i1.Mock ), returnValue: _i4.Future>.value(<_i5.XFile>[]), ) as _i4.Future>); + @override + bool supportsImageSource(_i2.ImageSource? source) => (super.noSuchMethod( + Invocation.method( + #supportsImageSource, + [source], + ), + returnValue: false, + ) as bool); } diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uuz(rC1}QWNE&K#jR^;j87-Auq zoUlN^K{r-Q+XN;zI ze|?*NFmgt#V#GwrSWaz^2G&@SBmck6ZcIFMww~vE<1E?M2#KUn1CzsB6D2+0SuRV@ zV2kK5HvIGB{HX-hQzs0*AB%5$9RJ@a;)Ahq#p$GSP91^&hi#6sg*;a~dt}4AclK>h z_3MoPRQ{i;==;*1S-mY<(JFzhAxMI&<61&m$J0NDHdJ3tYx~j0%M-uN6Zl8~_0DOkGXc0001@sz3l12C6Xg{AT~( zm6w64BA|AX`Ve)YY-glyudNN>MAfkXz-T7`_`fEolM;0T0BA)(02-OaW z0*cW7Z~ec94o8&g0D$N>b!COu{=m}^%oXZ4?T8ZyPZuGGBPBA7pbQMoV5HYhiT?%! zcae~`(QAN4&}-=#2f5fkn!SWGWmSeCISBcS=1-U|MEoKq=k?_x3apK>9((R zuu$9X?^8?@(a{qMS%J8SJPq))v}Q-ZyDm6Gbie0m92=`YlwnQPQP1kGSm(N2UJ3P6 z^{p-u)SSCTW~c1rw;cM)-uL2{->wCn2{#%;AtCQ!m%AakVs1K#v@(*-6QavyY&v&*wO_rCJXJuq$c$7ZjsW+pJo-$L^@!7X04CvaOpPyfw|FKvu;e(&Iw>Tbg zL}#8e^?X%TReXTt>gsBByt0kSU20oQx*~P=4`&tcZ7N6t-6LiK{LxX*p6}9c<0Pu^ zLx1w_P4P2V>bX=`F%v$#{sUDdF|;rbI{p#ZW`00Bgh(eB(nOIhy8W9T>3aQ=k8Z9% zB+TusFABF~J?N~fAd}1Rme=@4+1=M{^P`~se7}e3;mY0!%#MJf!XSrUC{0uZqMAd7%q zQY#$A>q}noIB4g54Ue)x>ofVm3DKBbUmS4Z-bm7KdKsUixva)1*&z5rgAG2gxG+_x zqT-KNY4g7eM!?>==;uD9Y4iI(Hu$pl8!LrK_Zb}5nv(XKW{9R144E!cFf36p{i|8pRL~p`_^iNo z{mf7y`#hejw#^#7oKPlN_Td{psNpNnM?{7{R-ICBtYxk>?3}OTH_8WkfaTLw)ZRTfxjW+0>gMe zpKg~`Bc$Y>^VX;ks^J0oKhB#6Ukt{oQhN+o2FKGZx}~j`cQB%vVsMFnm~R_1Y&Ml? zwFfb~d|dW~UktY@?zkau>Owe zRroi(<)c4Ux&wJfY=3I=vg)uh;sL(IYY9r$WK1$F;jYqq1>xT{LCkIMb3t2jN8d`9 z=4(v-z7vHucc_fjkpS}mGC{ND+J-hc_0Ix4kT^~{-2n|;Jmn|Xf9wGudDk7bi*?^+ z7fku8z*mbkGm&xf&lmu#=b5mp{X(AwtLTf!N`7FmOmX=4xwbD=fEo8CaB1d1=$|)+ z+Dlf^GzGOdlqTO8EwO?8;r+b;gkaF^$;+#~2_YYVH!hD6r;PaWdm#V=BJ1gH9ZK_9 zrAiIC-)z)hRq6i5+$JVmR!m4P>3yJ%lH)O&wtCyum3A*})*fHODD2nq!1@M>t@Za+ zH6{(Vf>_7!I-APmpsGLYpl7jww@s5hHOj5LCQXh)YAp+y{gG(0UMm(Ur z3o3n36oFwCkn+H*GZ-c6$Y!5r3z*@z0`NrB2C^q#LkOuooUM8Oek2KBk}o1PU8&2L z4iNkb5CqJWs58aR394iCU^ImDqV;q_Pp?pl=RB2372(Io^GA^+oKguO1(x$0<7w3z z)j{vnqEB679Rz4i4t;8|&Zg77UrklxY9@GDq(ZphH6=sW`;@uIt5B?7Oi?A0-BL}(#1&R;>2aFdq+E{jsvpNHjLx2t{@g1}c~DQcPNmVmy| zNMO@ewD^+T!|!DCOf}s9dLJU}(KZy@Jc&2Nq3^;vHTs}Hgcp`cw&gd7#N}nAFe3cM1TF%vKbKSffd&~FG9y$gLyr{#to)nxz5cCASEzQ}gz8O)phtHuKOW6p z@EQF(R>j%~P63Wfosrz8p(F=D|Mff~chUGn(<=CQbSiZ{t!e zeDU-pPsLgtc#d`3PYr$i*AaT!zF#23htIG&?QfcUk+@k$LZI}v+js|yuGmE!PvAV3 ztzh90rK-0L6P}s?1QH`Ot@ilbgMBzWIs zIs6K<_NL$O4lwR%zH4oJ+}JJp-bL6~%k&p)NGDMNZX7)0kni&%^sH|T?A)`z z=adV?!qnWx^B$|LD3BaA(G=ePL1+}8iu^SnnD;VE1@VLHMVdSN9$d)R(Wk{JEOp(P zm3LtAL$b^*JsQ0W&eLaoYag~=fRRdI>#FaELCO7L>zXe6w*nxN$Iy*Q*ftHUX0+N- zU>{D_;RRVPbQ?U+$^%{lhOMKyE5>$?U1aEPist+r)b47_LehJGTu>TcgZe&J{ z{q&D{^Ps~z7|zj~rpoh2I_{gAYNoCIJmio3B}$!5vTF*h$Q*vFj~qbo%bJCCRy509 zHTdDh_HYH8Zb9`}D5;;J9fkWOQi%Y$B1!b9+ESj+B@dtAztlY2O3NE<6HFiqOF&p_ zW-K`KiY@RPSY-p9Q99}Hcd05DT79_pfb{BV7r~?9pWh=;mcKBLTen%THFPo2NN~Nf zriOtFnqx}rtO|A6k!r6 zf-z?y-UD{dT0kT9FJ`-oWuPHbo+3wBS(}?2ql(+e@VTExmfnB*liCb zmeI+v5*+W_L;&kQN^ChW{jE0Mw#0Tfs}`9bk3&7UjxP^Ke(%eJu2{VnW?tu7Iqecm zB5|=-QdzK$=h50~{X3*w4%o1FS_u(dG2s&427$lJ?6bkLet}yYXCy)u_Io1&g^c#( z-$yYmSpxz{>BL;~c+~sxJIe1$7eZI_9t`eB^Pr0)5CuA}w;;7#RvPq|H6!byRzIJG ziQ7a4y_vhj(AL`8PhIm9edCv|%TX#f50lt8+&V+D4<}IA@S@#f4xId80oH$!_!q?@ zFRGGg2mTv&@76P7aTI{)Hu%>3QS_d)pQ%g8BYi58K~m-Ov^7r8BhX7YC1D3vwz&N8{?H*_U7DI?CI)+et?q|eGu>42NJ?K4SY zD?kc>h@%4IqNYuQ8m10+8xr2HYg2qFNdJl=Tmp&ybF>1>pqVfa%SsV*BY$d6<@iJA ziyvKnZ(~F9xQNokBgMci#pnZ}Igh0@S~cYcU_2Jfuf|d3tuH?ZSSYBfM(Y3-JBsC|S9c;# zyIMkPxgrq};0T09pjj#X?W^TFCMf1-9P{)g88;NDI+S4DXe>7d3Mb~i-h&S|Jy{J< zq3736$bH?@{!amD!1Ys-X)9V=#Z={fzsjVYMX5BG6%}tkzwC#1nQLj1y1f#}8**4Y zAvDZHw8)N)8~oWC88CgzbwOrL9HFbk4}h85^ptuu7A+uc#$f^9`EWv1Vr{5+@~@Uv z#B<;-nt;)!k|fRIg;2DZ(A2M2aC65kOIov|?Mhi1Sl7YOU4c$T(DoRQIGY`ycfkn% zViHzL;E*A{`&L?GP06Foa38+QNGA zw3+Wqs(@q+H{XLJbwZzE(omw%9~LPZfYB|NF5%j%E5kr_xE0u;i?IOIchn~VjeDZ) zAqsqhP0vu2&Tbz3IgJvMpKbThC-@=nk)!|?MIPP>MggZg{cUcKsP8|N#cG5 zUXMXxcXBF9`p>09IR?x$Ry3;q@x*%}G#lnB1}r#!WL88I@uvm}X98cZ8KO&cqT1p> z+gT=IxPsq%n4GWgh-Bk8E4!~`r@t>DaQKsjDqYc&h$p~TCh8_Mck5UB84u6Jl@kUZCU9BA-S!*bf>ZotFX9?a_^y%)yH~rsAz0M5#^Di80_tgoKw(egN z`)#(MqAI&A84J#Z<|4`Co8`iY+Cv&iboMJ^f9ROUK0Lm$;-T*c;TCTED_0|qfhlcS zv;BD*$Zko#nWPL}2K8T-?4}p{u)4xon!v_(yVW8VMpxg4Kh^J6WM{IlD{s?%XRT8P|yCU`R&6gwB~ zg}{At!iWCzOH37!ytcPeC`(({ovP7M5Y@bYYMZ}P2Z3=Y_hT)4DRk}wfeIo%q*M9UvXYJq!-@Ly79m5aLD{hf@BzQB>FdQ4mw z6$@vzSKF^Gnzc9vbccii)==~9H#KW<6)Uy1wb~auBn6s`ct!ZEos`WK8e2%<00b%# zY9Nvnmj@V^K(a_38dw-S*;G-(i(ETuIwyirs?$FFW@|66a38k+a%GLmucL%Wc8qk3 z?h_4!?4Y-xt)ry)>J`SuY**fuq2>u+)VZ+_1Egzctb*xJ6+7q`K$^f~r|!i?(07CD zH!)C_uerf-AHNa?6Y61D_MjGu*|wcO+ZMOo4q2bWpvjEWK9yASk%)QhwZS%N2_F4& z16D18>e%Q1mZb`R;vW{+IUoKE`y3(7p zplg5cBB)dtf^SdLd4n60oWie|(ZjgZa6L*VKq02Aij+?Qfr#1z#fwh92aV-HGd^_w zsucG24j8b|pk>BO7k8dS86>f-jBP^Sa}SF{YNn=^NU9mLOdKcAstv&GV>r zLxKHPkFxpvE8^r@MSF6UA}cG`#yFL8;kA7ccH9D=BGBtW2;H>C`FjnF^P}(G{wU;G z!LXLCbPfsGeLCQ{Ep$^~)@?v`q(uI`CxBY44osPcq@(rR-633!qa zsyb>?v%@X+e|Mg`+kRL*(;X>^BNZz{_kw5+K;w?#pReiw7eU8_Z^hhJ&fj80XQkuU z39?-z)6Fy$I`bEiMheS(iB6uLmiMd1i)cbK*9iPpl+h4x9ch7x- z1h4H;W_G?|)i`z??KNJVwgfuAM=7&Apd3vm#AT8uzQZ!NII}}@!j)eIfn53h{NmN7 zAKG6SnKP%^k&R~m5#@_4B@V?hYyHkm>0SQ@PPiw*@Tp@UhP-?w@jW?nxXuCipMW=L zH*5l*d@+jXm0tIMP_ec6Jcy6$w(gKK@xBX8@%oPaSyG;13qkFb*LuVx3{AgIyy&n3 z@R2_DcEn|75_?-v5_o~%xEt~ONB>M~tpL!nOVBLPN&e5bn5>+7o0?Nm|EGJ5 zmUbF{u|Qn?cu5}n4@9}g(G1JxtzkKv(tqwm_?1`?YSVA2IS4WI+*(2D*wh&6MIEhw z+B+2U<&E&|YA=3>?^i6)@n1&&;WGHF-pqi_sN&^C9xoxME5UgorQ_hh1__zzR#zVC zOQt4q6>ME^iPJ37*(kg4^=EFqyKH@6HEHXy79oLj{vFqZGY?sVjk!BX^h$SFJlJnv z5uw~2jLpA)|0=tp>qG*tuLru?-u`khGG2)o{+iDx&nC}eWj3^zx|T`xn5SuR;Aw8U z`p&>dJw`F17@J8YAuW4=;leBE%qagVTG5SZdh&d)(#ZhowZ|cvWvGMMrfVsbg>_~! z19fRz8CSJdrD|Rl)w!uznBF&2-dg{>y4l+6(L(vzbLA0Bk&`=;oQQ>(M8G=3kto_) zP8HD*n4?MySO2YrG6fwSrVmnesW+D&fxjfEmp=tPd?RKLZJcH&K(-S+x)2~QZ$c(> zru?MND7_HPZJVF%wX(49H)+~!7*!I8w72v&{b={#l9yz+S_aVPc_So%iF8>$XD1q1 zFtucO=rBj0Ctmi0{njN8l@}!LX}@dwl>3yMxZ;7 z0Ff2oh8L)YuaAGOuZ5`-p%Z4H@H$;_XRJQ|&(MhO78E|nyFa158gAxG^SP(vGi^+< zChY}o(_=ci3Wta#|K6MVljNe0T$%Q5ylx-v`R)r8;3+VUpp-)7T`-Y&{Zk z*)1*2MW+_eOJtF5tCMDV`}jg-R(_IzeE9|MBKl;a7&(pCLz}5<Zf+)T7bgNUQ_!gZtMlw=8doE}#W+`Xp~1DlE=d5SPT?ymu!r4z%&#A-@x^=QfvDkfx5-jz+h zoZ1OK)2|}_+UI)i9%8sJ9X<7AA?g&_Wd7g#rttHZE;J*7!e5B^zdb%jBj&dUDg4&B zMMYrJ$Z%t!5z6=pMGuO-VF~2dwjoXY+kvR>`N7UYfIBMZGP|C7*O=tU z2Tg_xi#Q3S=1|=WRfZD;HT<1D?GMR%5kI^KWwGrC@P2@R>mDT^3qsmbBiJc21kip~ zZp<7;^w{R;JqZ)C4z-^wL=&dBYj9WJBh&rd^A^n@07qM$c+kGv^f+~mU5_*|eePF| z3wDo-qaoRjmIw<2DjMTG4$HP{z54_te_{W^gu8$r=q0JgowzgQPct2JNtWPUsjF8R zvit&V8$(;7a_m%%9TqPkCXYUp&k*MRcwr*24>hR! z$4c#E=PVE=P4MLTUBM z7#*RDe0}=B)(3cvNpOmWa*eH#2HR?NVqXdJ=hq);MGD07JIQQ7Y0#iD!$C+mk7x&B zMwkS@H%>|fmSu#+ zI!}Sb(%o29Vkp_Th>&&!k7O>Ba#Om~B_J{pT7BHHd8(Ede(l`7O#`_}19hr_?~JP9 z`q(`<)y>%)x;O7)#-wfCP{?llFMoH!)ZomgsOYFvZ1DxrlYhkWRw#E-#Qf*z@Y-EQ z1~?_=c@M4DO@8AzZ2hKvw8CgitzI9yFd&N1-{|vP#4IqYb*#S0e3hrjsEGlnc4xwk z4o!0rxpUt8j&`mJ8?+P8G{m^jbk)bo_UPM+ifW*y-A*et`#_Ja_3nYyRa9fAG1Xr5 z>#AM_@PY|*u)DGRWJihZvgEh#{*joJN28uN7;i5{kJ*Gb-TERfN{ERe_~$Es~NJCpdKLRvdj4658uYYx{ng7I<6j~w@p%F<7a(Ssib|j z51;=Py(Nu*#hnLx@w&8X%=jrADn3TW>kplnb zYbFIWWVQXN7%Cwn6KnR)kYePEBmvM45I)UJb$)ninpdYg3a5N6pm_7Q+9>!_^xy?k za8@tJ@OOs-pRAAfT>Nc2x=>sZUs2!9Dwa%TTmDggH4fq(x^MW>mcRyJINlAqK$YQCMgR8`>6=Sg$ zFnJZsA8xUBXIN3i70Q%8px@yQPMgVP=>xcPI38jNJK<=6hC={a07+n@R|$bnhB)X$ z(Zc%tadp70vBTnW{OUIjTMe38F}JIH$#A}PB&RosPyFZMD}q}5W%$rh>5#U;m`z2K zc(&WRxx7DQLM-+--^w*EWAIS%bi>h587qkwu|H=hma3T^bGD&Z!`u(RKLeNZ&pI=q$|HOcji(0P1QC!YkAp*u z3%S$kumxR}jU<@6`;*-9=5-&LYRA<~uFrwO3U0k*4|xUTp4ZY7;Zbjx|uw&BWU$zK(w55pWa~#=f$c zNDW0O68N!xCy>G}(CX=;8hJLxAKn@Aj(dbZxO8a$+L$jK8$N-h@4$i8)WqD_%Snh4 zR?{O%k}>lr>w$b$g=VP8mckcCrjnp>uQl5F_6dPM8FWRqs}h`DpfCv20uZhyY~tr8 zkAYW4#yM;*je)n=EAb(q@5BWD8b1_--m$Q-3wbh1hM{8ihq7UUQfg@)l06}y+#=$( z$x>oVYJ47zAC^>HLRE-!HitjUixP6!R98WU+h>zct7g4eD;Mj#FL*a!VW!v-@b(Jv zj@@xM5noCp5%Vk3vY{tyI#oyDV7<$`KG`tktVyC&0DqxA#>V;-3oH%NW|Q&=UQ&zU zXNIT67J4D%5R1k#bW0F}TD`hlW7b)-=-%X4;UxQ*u4bK$mTAp%y&-(?{sXF%e_VH6 zTkt(X)SSN|;8q@8XX6qfR;*$r#HbIrvOj*-5ND8RCrcw4u8D$LXm5zlj@E5<3S0R# z??=E$p{tOk96$SloZ~ARe5`J=dB|Nj?u|zy2r(-*(q^@YwZiTF@QzQyPx_l=IDKa) zqD@0?IHJqSqZ_5`)81?4^~`yiGh6>7?|dKa8!e|}5@&qV!Iu9<@G?E}Vx9EzomB3t zEbMEm$TKGwkHDpirp;FZD#6P5qIlQJ8}rf;lHoz#h4TFFPYmS3+8(13_Mx2`?^=8S z|0)0&dQLJTU6{b%*yrpQe#OKKCrL8}YKw+<#|m`SkgeoN69TzIBQOl_Yg)W*w?NW) z*WxhEp$zQBBazJSE6ygu@O^!@Fr46j=|K`Mmb~xbggw7<)BuC@cT@Bwb^k?o-A zKX^9AyqR?zBtW5UA#siILztgOp?r4qgC`9jYJG_fxlsVSugGprremg-W(K0{O!Nw-DN%=FYCyfYA3&p*K>+|Q}s4rx#CQK zNj^U;sLM#q8}#|PeC$p&jAjqMu(lkp-_50Y&n=qF9`a3`Pr9f;b`-~YZ+Bb0r~c+V z*JJ&|^T{}IHkwjNAaM^V*IQ;rk^hnnA@~?YL}7~^St}XfHf6OMMCd9!vhk#gRA*{L zp?&63axj|Si%^NW05#87zpU_>QpFNb+I00v@cHwvdBn+Un)n2Egdt~LcWOeBW4Okm zD$-e~RD+W|UB;KQ;a7GOU&%p*efGu2$@wR74+&iP8|6#_fmnh^WcJLs)rtz{46);F z4v0OL{ZP9550>2%FE(;SbM*#sqMl*UXOb>ch`fJ|(*bOZ9=EB1+V4fkQ)hjsm3-u^Pk-4ji_uDDHdD>84tER!MvbH`*tG zzvbhBR@}Yd`azQGavooV=<WbvWLlO#x`hyO34mKcxrGv=`{ssnP=0Be5#1B;Co9 zh{TR>tjW2Ny$ZxJpYeg57#0`GP#jxDCU0!H15nL@@G*HLQcRdcsUO3sO9xvtmUcc{F*>FQZcZ5bgwaS^k-j5mmt zI7Z{Xnoml|A(&_{imAjK!kf5>g(oDqDI4C{;Bv162k8sFNr;!qPa2LPh>=1n z=^_9)TsLDvTqK7&*Vfm5k;VXjBW^qN3Tl&}K=X5)oXJs$z3gk0_+7`mJvz{pK|FVs zHw!k&7xVjvY;|(Py<;J{)b#Yjj*LZO7x|~pO4^MJ2LqK3X;Irb%nf}L|gck zE#55_BNsy6m+W{e zo!P59DDo*s@VIi+S|v93PwY6d?CE=S&!JLXwE9{i)DMO*_X90;n2*mPDrL%{iqN!?%-_95J^L z=l<*{em(6|h7DR4+4G3Wr;4*}yrBkbe3}=p7sOW1xj!EZVKSMSd;QPw>uhKK z#>MlS@RB@-`ULv|#zI5GytO{=zp*R__uK~R6&p$q{Y{iNkg61yAgB8C^oy&``{~FK z8hE}H&nIihSozKrOONe5Hu?0Zy04U#0$fB7C6y~?8{or}KNvP)an=QP&W80mj&8WL zEZQF&*FhoMMG6tOjeiCIV;T{I>jhi9hiUwz?bkX3NS-k5eWKy)Mo_orMEg4sV6R6X&i-Q%JG;Esl+kLpn@Bsls9O|i9z`tKB^~1D5)RIBB&J<6T@a4$pUvh$IR$%ubH)joi z!7>ON0DPwx=>0DA>Bb^c?L8N0BBrMl#oDB+GOXJh;Y&6I)#GRy$W5xK%a;KS8BrER zX)M>Rdoc*bqP*L9DDA3lF%U8Yzb6RyIsW@}IKq^i7v&{LeIc=*ZHIbO68x=d=+0T( zev=DT9f|x!IWZNTB#N7}V4;9#V$%Wo0%g>*!MdLOEU>My0^gni9ocID{$g9ytD!gy zKRWT`DVN(lcYjR|(}f0?zgBa3SwunLfAhx><%u0uFkrdyqlh8_g zDKt#R6rA2(Vm2LW_>3lBNYKG_F{TEnnKWGGC15y&OebIRhFL4TeMR*v9i0wPoK#H< zu4){s4K&K)K(9~jgGm;H7lS7y_RYfS;&!Oj5*eqbvEcW^a*i67nevzOZxN6F+K~A%TYEtsAVsR z@J=1hc#Dgs7J2^FL|qV&#WBFQyDtEQ2kPO7m2`)WFhqAob)Y>@{crkil6w9VoA?M6 zADGq*#-hyEVhDG5MQj677XmcWY1_-UO40QEP&+D)rZoYv^1B_^w7zAvWGw&pQyCyx zD|ga$w!ODOxxGf_Qq%V9Z7Q2pFiUOIK818AGeZ-~*R zI1O|SSc=3Z?#61Rd|AXx2)K|F@Z1@x!hBBMhAqiU)J=U|Y)T$h3D?ZPPQgkSosnN! zIqw-t$0fqsOlgw3TlHJF*t$Q@bg$9}A3X=cS@-yU3_vNG_!#9}7=q7!LZ?-%U26W4 z$d>_}*s1>Ac%3uFR;tnl*fNlylJ)}r2^Q3&@+is3BIv<}x>-^_ng;jhdaM}6Sg3?p z0jS|b%QyScy3OQ(V*~l~bK>VC{9@FMuW_JUZO?y(V?LKWD6(MXzh}M3r3{7b4eB(#`(q1m{>Be%_<9jw8HO!x#yF6vez$c#kR+}s zZO-_;25Sxngd(}){zv?ccbLqRAlo;yog>4LH&uZUK1n>x?u49C)Y&2evH5Zgt~666 z_2_z|H5AO5Iqxv_Bn~*y1qzRPcob<+Otod5Xd2&z=C;u+F}zBB@b^UdGdUz|s!H}M zXG%KiLzn3G?FZgdY&3pV$nSeY?ZbU^jhLz9!t0K?ep}EFNqR1@E!f*n>x*!uO*~JF zW9UXWrVgbX1n#76_;&0S7z}(5n-bqnII}_iDsNqfmye@)kRk`w~1 z6j4h4BxcPe6}v)xGm%=z2#tB#^KwbgMTl2I*$9eY|EWAHFc3tO48Xo5rW z5oHD!G4kb?MdrOHV=A+8ThlIqL8Uu+7{G@ zb)cGBm|S^Eh5= z^E^SZ=yeC;6nNCdztw&TdnIz}^Of@Ke*@vjt)0g>Y!4AJvWiL~e7+9#Ibhe)> ziNwh>gWZL@FlWc)wzihocz+%+@*euwXhW%Hb>l7tf8aJe5_ZSH1w-uG|B;9qpcBP0 zM`r1Hu#htOl)4Cl1c7oY^t0e4Jh$-I(}M5kzWqh{F=g&IM#JiC`NDSd@BCKX#y<P@Gwl$3a3w z6<(b|K(X5FIR22M)sy$4jY*F4tT{?wZRI+KkZFb<@j@_C316lu1hq2hA|1wCmR+S@ zRN)YNNE{}i_H`_h&VUT5=Y(lN%m?%QX;6$*1P}K-PcPx>*S55v)qZ@r&Vcic-sjkm z! z=nfW&X`}iAqa_H$H%z3Tyz5&P3%+;93_0b;zxLs)t#B|up}JyV$W4~`8E@+BHQ+!y zuIo-jW!~)MN$2eHwyx-{fyGjAWJ(l8TZtUp?wZWBZ%}krT{f*^fqUh+ywHifw)_F> zp76_kj_B&zFmv$FsPm|L7%x-j!WP>_P6dHnUTv!9ZWrrmAUteBa`rT7$2ixO;ga8U z3!91micm}{!Btk+I%pMgcKs?H4`i+=w0@Ws-CS&n^=2hFTQ#QeOmSz6ttIkzmh^`A zYPq)G1l3h(E$mkyr{mvz*MP`x+PULBn%CDhltKkNo6Uqg!vJ#DA@BIYr9TQ`18Un2 zv$}BYzOQuay9}w(?JV63F$H6WmlYPPpH=R|CPb%C@BCv|&Q|&IcW7*LX?Q%epS z`=CPx{1HnJ9_46^=0VmNb>8JvMw-@&+V8SDLRYsa>hZXEeRbtf5eJ>0@Ds47zIY{N z42EOP9J8G@MXXdeiPx#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$?lu1NER9Fe^SItioK@|V(ZWmgL zZT;XwPgVuWM>O%^|Dc$VK;n&?9!&g5)aVsG8cjs5UbtxVVnQNOV~7Mrg3+jnU;rhE z6fhW6P)R>_eXrXo-RW*y6RQ_qcb^s1wTu$TwriZ`=JUws>vRi}5x}MW1MR#7p|gIWJlaLK;~xaN}b< z<-@=RX-%1mt`^O0o^~2=CD7pJ<<$Rp-oUL-7PuG>do^5W_Mk#unlP}6I@6NPxY`Q} zuXJF}!0l)vwPNAW;@5DjPRj?*rZxl zwn;A(cFV!xe^CUu+6SrN?xe#mz?&%N9QHf~=KyK%DoB8HKC)=w=3E?1Bqj9RMJs3U z5am3Uv`@+{jgqO^f}Lx_Jp~CoP3N4AMZr~4&d)T`R?`(M{W5WWJV^z~2B|-oih@h^ zD#DuzGbl(P5>()u*YGo*Och=oRr~3P1wOlKqI)udc$|)(bacG5>~p(y>?{JD7nQf_ z*`T^YL06-O>T(s$bi5v~_fWMfnE7Vn%2*tqV|?~m;wSJEVGkNMD>+xCu#um(7}0so zSEu7?_=Q64Q5D+fz~T=Rr=G_!L*P|(-iOK*@X8r{-?oBlnxMNNgCVCN9Y~ocu+?XA zjjovJ9F1W$Nf!{AEv%W~8oahwM}4Ruc+SLs>_I_*uBxdcn1gQ^2F8a*vGjgAXYyh? zWCE@c5R=tbD(F4nL9NS?$PN1V_2*WR?gjv3)4MQeizuH`;sqrhgykEzj z593&TGlm3h`sIXy_U<7(dpRXGgp0TB{>s?}D{fwLe>IV~exweOfH!qM@CV5kib!YA z6O0gvJi_0J8IdEvyP#;PtqP*=;$iI2t(xG2YI-e!)~kaUn~b{6(&n zp)?iJ`z2)Xh%sCV@BkU`XL%_|FnCA?cVv@h*-FOZhY5erbGh)%Q!Av#fJM3Csc_g zC2I6x%$)80`Tkz#KRA!h1FzY`?0es3t!rKDT5EjPe6B=BLPr7s0GW!if;Ip^!AmGW zL;$`Vdre+|FA!I4r6)keFvAx3M#1`}ijBHDzy)3t0gwjl|qC2YB`SSxFKHr(oY#H$)x{L$LL zBdLKTlsOrmb>T0wd=&6l3+_Te>1!j0OU8%b%N342^opKmT)gni(wV($s(>V-fUv@0p8!f`=>PxC|9=nu ze{ToBBj8b<{PLfXV$h8YPgA~E!_sF9bl;QOF{o6t&JdsX?}rW!_&d`#wlB6T_h;Xf zl{4Tz5>qjF4kZgjO7ZiLPRz_~U@k5%?=30+nxEh9?s78gZ07YHB`FV`4%hlQlMJe@J`+e(qzy+h(9yY^ckv_* zb_E6o4p)ZaWfraIoB2)U7_@l(J0O%jm+Or>8}zSSTkM$ASG^w3F|I? z$+eHt7T~04(_WfKh27zqS$6* zzyy-ZyqvSIZ0!kkSvHknm_P*{5TKLQs8S6M=ONuKAUJWtpxbL#2(_huvY(v~Y%%#~ zYgsq$JbLLprKkV)32`liIT$KKEqs$iYxjFlHiRNvBhxbDg*3@Qefw4UM$>i${R5uB zhvTgmqQsKA{vrKN;TSJU2$f9q=y{$oH{<)woSeV>fkIz6D8@KB zf4M%v%f5U2?<8B(xn}xV+gWP?t&oiapJhJbfa;agtz-YM7=hrSuxl8lAc3GgFna#7 zNjX7;`d?oD`#AK+fQ=ZXqfIZFEk{ApzjJF0=yO~Yj{7oQfXl+6v!wNnoqwEvrs81a zGC?yXeSD2NV!ejp{LdZGEtd1TJ)3g{P6j#2jLR`cpo;YX}~_gU&Gd<+~SUJVh+$7S%`zLy^QqndN<_9 zrLwnXrLvW+ew9zX2)5qw7)zIYawgMrh`{_|(nx%u-ur1B7YcLp&WFa24gAuw~& zKJD3~^`Vp_SR$WGGBaMnttT)#fCc^+P$@UHIyBu+TRJWbcw4`CYL@SVGh!X&y%!x~ zaO*m-bTadEcEL6V6*{>irB8qT5Tqd54TC4`h`PVcd^AM6^Qf=GS->x%N70SY-u?qr>o2*OV7LQ=j)pQGv%4~z zz?X;qv*l$QSNjOuQZ>&WZs2^@G^Qas`T8iM{b19dS>DaXX~=jd4B2u`P;B}JjRBi# z_a@&Z5ev1-VphmKlZEZZd2-Lsw!+1S60YwW6@>+NQ=E5PZ+OUEXjgUaXL-E0fo(E* zsjQ{s>n33o#VZm0e%H{`KJi@2ghl8g>a~`?mFjw+$zlt|VJhSU@Y%0TWs>cnD&61fW4e0vFSaXZa4-c}U{4QR8U z;GV3^@(?Dk5uc@RT|+5C8-24->1snH6-?(nwXSnPcLn#X_}y3XS)MI_?zQ$ZAuyg+ z-pjqsw}|hg{$~f0FzmmbZzFC0He_*Vx|_uLc!Ffeb8#+@m#Z^AYcWcZF(^Os8&Z4g zG)y{$_pgrv#=_rV^D|Y<_b@ICleUv>c<0HzJDOsgJb#Rd-Vt@+EBDPyq7dUM9O{Yp zuGUrO?ma2wpuJuwl1M=*+tb|qx7Doj?!F-3Z>Dq_ihFP=d@_JO;vF{iu-6MWYn#=2 zRX6W=`Q`q-+q@Db|6_a1#8B|#%hskH82lS|9`im0UOJn?N#S;Y0$%xZw3*jR(1h5s z?-7D1tnIafviko>q6$UyqVDq1o@cwyCb*})l~x<@s$5D6N=-Uo1yc49p)xMzxwnuZ zHt!(hu-Ek;Fv4MyNTgbW%rPF*dB=;@r3YnrlFV{#-*gKS_qA(G-~TAlZ@Ti~Yxw;k za1EYyX_Up|`rpbZ0&Iv#$;eC|c0r4XGaQ-1mw@M_4p3vKIIpKs49a8Ns#ni)G314Z z8$Ei?AhiT5dQGWUYdCS|IC7r z=-8ol>V?u!n%F*J^^PZ(ONT&$Ph;r6X;pj|03HlDY6r~0g~X#zuzVU%a&!fs_f|m?qYvg^Z{y?9Qh7Rn?T*F%7lUtA6U&={HzhYEzA`knx1VH> z{tqv?p@I(&ObD5L4|YJV$QM>Nh-X3cx{I&!$FoPC_2iIEJfPk-$;4wz>adRu@n`_y z_R6aN|MDHdK;+IJmyw(hMoDCFCQ(6?hCAG5&7p{y->0Uckv# zvooVuu04$+pqof777ftk<#42@KQ((5DPcSMQyzGOJ{e9H$a9<2Qi_oHjl{#=FUL9d z+~0^2`tcvmp0hENwfHR`Ce|<1S@p;MNGInXCtHnrDPXCKmMTZQ{HVm_cZ>@?Wa6}O zHsJc7wE)mc@1OR2DWY%ZIPK1J2p6XDO$ar`$RXkbW}=@rFZ(t85AS>>U0!yt9f49^ zA9@pc0P#k;>+o5bJfx0t)Lq#v4`OcQn~av__dZ-RYOYu}F#pdsl31C^+Qgro}$q~5A<*c|kypzd} ziYGZ~?}5o`S5lw^B{O@laad9M_DuJle- z*9C7o=CJh#QL=V^sFlJ0c?BaB#4bV^T(DS6&Ne&DBM_3E$S^S13qC$7_Z?GYXTpR@wqr70wu$7+qvf-SEUa5mdHvFbu^7ew!Z1a^ zo}xKOuT*gtGws-a{Tx}{#(>G~Y_h&5P@Q8&p!{*s37^QX_Ibx<6XU*AtDOIvk|^{~ zPlS}&DM5$Ffyu-T&0|KS;Wnaqw{9DB&B3}vcO14wn;)O_e@2*9B&0I_ zZz{}CMxx`hv-XouY>^$Y@J(_INeM>lIQI@I>dBAqq1)}?Xmx(qRuX^i4IV%=MF306 z9g)i*79pP%_7Ex?m6ag-4Tlm=Z;?DQDyC-NpUIb#_^~V_tsL<~5<&;Gf2N+p?(msn zzUD~g>OoW@O}y0@Z;RN)wjam`CipmT&O7a|YljZqU=U86 zedayEdY)2F#BJ6xvmW8K&ffdS*0!%N<%RB!2~PAT4AD*$W7yzHbX#Eja9%3aD+Ah2 zf#T;XJW-GMxpE=d4Y>}jE=#U`IqgSoWcuvgaWQ9j1CKzG zDkoMDDT)B;Byl3R2PtC`ip=yGybfzmVNEx{xi_1|Cbqj>=FxQc{g`xj6fIfy`D8fA z##!-H_e6o0>6Su&$H2kQTujtbtyNFeKc}2=|4IfLTnye#@$Au7Kv4)dnA;-fz@D_8 z)>irG$)dkBY~zX zC!ZXLy*L3xr6cb70QqfN#Q>lFIc<>}>la4@3%7#>a1$PU&O^&VszpxLC%*!m-cO{B z-Y}rQr4$84(hvy#R69H{H zJ*O#uJh)TF6fbXy;fZkk%X=CjsTK}o5N1a`d7kgYYZLPxsHx%9*_XN8VWXEkVJZ%A z1A+5(B;0^{T4aPYr8%i@i32h)_)|q?9vws)r+=5u)1YNftF5mknwfd*%jXA2TeP}Z zQ!m?xJ3?9LpPM?_A3$hQ1QxNbR&}^m z!F999s?p^ak#C4NM_x2p9FoXWJ$>r?lJ)2bG)sX{gExgLA2s5RwHV!h6!C~d_H||J z>9{E{mEv{Z1z~65Vix@dqM4ZqiU|!)eWX$mwS5mLSufxbpBqqS!jShq1bmwCR6 z4uBri7ezMeS6ycaXPVu(i2up$L; zjpMtB`k~WaNrdgM_R=e#SN?Oa*u%nQy01?()h4A(jyfeNfx;5o+kX?maO4#1A^L}0 zYNyIh@QVXIFiS0*tE}2SWTrWNP3pH}1Vz1;E{@JbbgDFM-_Mky^7gH}LEhl~Ve5PexgbIyZ(IN%PqcaV@*_`ZFb=`EjspSz%5m2E34BVT)d=LGyHVz@-e%9Ova*{5@RD;7=Ebkc2GP%pIP^P7KzKapnh`UpH?@h z$RBpD*{b?vhohOKf-JG3?A|AX|2pQ?(>dwIbWhZ38GbTm4AImRNdv_&<99ySX;kJ| zo|5YgbHZC#HYgjBZrvGAT4NZYbp}qkVSa;C-LGsR26Co+i_HM&{awuO9l)Ml{G8zD zs$M8R`r+>PT#Rg!J(K6T4xHq7+tscU(}N$HY;Yz*cUObX7J7h0#u)S7b~t^Oj}TBF zuzsugnst;F#^1jm>22*AC$heublWtaQyM6RuaquFd8V#hJ60Z3j7@bAs&?dD#*>H0SJaDwp%U~27>zdtn+ z|8sZzklZy$%S|+^ie&P6++>zbrq&?+{Yy11Y>@_ce@vU4ZulS@6yziG6;iu3Iu`M= zf3rcWG<+3F`K|*(`0mE<$89F@jSq;j=W#E>(R}2drCB7D*0-|D;S;(;TwzIJkGs|q z2qH{m_zZ+el`b;Bv-#bQ>}*VPYC|7`rgBFf2oivXS^>v<&HHTypvd4|-zn|=h=TG{ z05TH2+{T%EnADO>3i|CB zCu60#qk`}GW{n4l-E$VrqgZGbI zbQW690KgZt4U3F^5@bdO1!xu~p@7Y~*_FfWg2CdvED5P5#w#V46LH`<&V0{t&Ml~4 zHNi7lIa+#i+^Z6EnxO7KJQw)wD)4~&S-Ki8)3=jpqxmx6c&zU&<&h%*c$I(5{1HZT zc9WE}ijcWJiVa^Q^xC|WX0habl89qycOyeViIbi(LFsEY_8a|+X^+%Qv+W4vzj>`y zpuRnjc-eHNkvXvI_f{=*FX=OKQzT?bck#2*qoKTHmDe>CDb&3AngA1O)1b}QJ1Tun z_<@yVEM>qG7664Pa@dzL@;DEh`#?yM+M|_fQS<7yv|i*pw)|Z8)9IR+QB7N3v3K(wv4OY*TXnH&X0nQB}?|h2XQeGL^q~N7N zDFa@x0E(UyN7k9g%IFq7Sf+EAfE#K%%#`)!90_)Dmy3Bll&e1vHQyPA87TaF(xbqMpDntVp?;8*$87STop$!EAnGhZ?>mqPJ(X zFsr336p3P{PpZCGn&^LP(JjnBbl_3P3Kcq+m}xVFMVr1zdCPJMDIV_ki#c=vvTwbU z*gKtfic&{<5ozL6Vfpx>o2Tts?3fkhWnJD&^$&+Mh5WGGyO7fG@6WDE`tEe(8<;+q z@Ld~g08XDzF8xtmpIj`#q^(Ty{Hq>t*v`pedHnuj(0%L(%sjkwp%s}wMd!a<*L~9T z9MM@s)Km~ogxlqEhIw5(lc46gCPsSosUFsgGDr8H{mj%OzJz{N#;bQ;KkV+ZWA1(9 zu0PXzyh+C<4OBYQ0v3z~Lr;=C@qmt8===Ov2lJ1=DeLfq*#jgT{YQCuwz?j{&3o_6 zsqp2Z_q-YWJg?C6=!Or|b@(zxTlg$ng2eUQzuC<+o)k<6^9ju_Z*#x+oioZ5T8Z_L zz9^A1h2eFS0O5muq8;LuDKwOv4A9pxmOjgb6L*i!-(0`Ie^d5Fsgspon%X|7 zC{RRXEmYn!5zP9XjG*{pLa)!2;PJB2<-tH@R7+E1cRo=Wz_5Ko8h8bB$QU%t9#vol zAoq?C$~~AsYC|AQQ)>>7BJ@{Cal)ZpqE=gjT+Juf!RD-;U0mbV1ED5PbvFD6M=qj1 zZ{QERT5@(&LQ~1X9xSf&@%r|3`S#ZCE=sWD`D4YQZ`MR`G&s>lN{y2+HqCfvgcw3E z-}Kp(dfGG?V|97kAHQX+OcKCZS`Q%}HD6u*e$~Ki&Vx53&FC!x94xJd4F2l^qQeFO z?&JdmgrdVjroKNJx64C!H&Vncr^w zzR#XI}Dn&o8jB~_YlVM^+#0W(G1LZH5K^|uYT@KSR z^Y5>^*Bc45E1({~EJB(t@4n9gb-eT#s@@7)J^^<_VV`Pm!h7av8XH6^5zO zOcQBhTGr;|MbRsgxCW69w{bl4EW#A~);L?d4*y#j8Ne=Z@fmJP0k4{_cQ~KA|Y#_#BuUiYx8y*za3_6Y}c=GSe7(2|KAfhdzud!Zq&}j)=o4 z7R|&&oX7~e@~HmyOOsCCwy`AR+deNjZ3bf6ijI_*tKP*_5JP3;0d;L_p(c>W1b%sG zJ*$wcO$ng^aW0E(5ldckV9unU7}OB7s?Wx(761?1^&8tA5y0_(ieV>(x-e@}1`lWC z-YH~G$D>#ud!SxK2_Iw{K%92=+{4yb-_XC>ji&j7)1ofp(OGa4jjF;Hd*`6YQL+Jf zffg+6CPc8F@EDPN{Kn96yip;?g@)qgkPo^nVKFqY?8!=h$G$V=<>%5J&iVjwR!7H0 z$@QL|_Q81I;Bnq8-5JyNRv$Y>`sWl{qhq>u+X|)@cMlsG!{*lu?*H`Tp|!uv z9oEPU1jUEj@ueBr}%Y)7Luyi)REaJV>eQ{+uy4uh0ep0){t;OU8D*RZ& zE-Z-&=BrWQLAD^A&qut&4{ZfhqK1ZQB0fACP)=zgx(0(o-`U62EzTkBkG@mXqbjXm z>w`HNeQM?Is&4xq@BB(K;wv5nI6EXas)XXAkUuf}5uSrZLYxRCQPefn-1^#OCd4aO zzF=dQ*CREEyWf@n6h7(uXLNgJIwGp#Xrsj6S<^bzQ7N0B0N{XlT;`=m9Olg<>KL}9 zlp>EKTx-h|%d1Ncqa=wnQEuE;sIO-f#%Bs?g4}&xS?$9MG?n$isHky0caj za8W+B^ERK#&h?(x)7LLpOqApV5F>sqB`sntV%SV>Q1;ax67qs+WcssfFeF3Xk=e4^ zjR2^(%K1oBq%0%Rf!y&WT;lu2Co(rHi|r1_uW)n{<7fGc-c=ft7Z0Q}r4W$o$@tQF#i?jDBwZ8h+=SC}3?anUp3mtRVv9l#H?-UD;HjTF zQ*>|}e=6gDrgI9p%c&4iMUkQa4zziS$bO&i#DI$Wu$7dz7-}XLk%!US^XUIFf2obO zFCTjVEtkvYSKWB;<0C;_B{HHs~ax_48^Cml*mjfBC5*7^HJZiLDir(3k&BerVIZF8zF;0q80eX8c zPN4tc+Dc5DqEAq$Y3B3R&XPZ=AQfFMXv#!RQnGecJONe0H;+!f^h5x0wS<+%;D}MpUbTNUBA}S2n&U59-_5HKr{L^jPsV8B^%NaH|tUr)mq=qCBv_- ziZ1xUp(ZzxUYTCF@C}To;u60?RIfTGS?#JnB8S8@j`TKPkAa)$My+6ziGaBcA@){d z91)%+v2_ba7gNecdj^8*I4#<11l!{XKl6s0zkXfJPxhP+@b+5ev{a>p*W-3*25c&} zmCf{g9mPWVQ$?Sp*4V|lT@~>RR)9iNdN^7KT@>*MU3&v^3e?=NTbG9!h6C|9zO097 zN{Qs6YwR-5$)~ z`b~qs`a1Dbx8P>%V=1XGjBptMf%P~sl1qbHVm1HYpY|-Z^Dar8^HqjIw}xaeRlsYa zJ_@Apy-??`gxPmb`m`0`z`#G7*_C}qiSZe~l2z65tE~IwMw$1|-u&t|z-8SxliH00 zlh1#kuqB56s+E&PWQ7Nz17?c}pN+A@-c^xLqh(j;mS|?>(Pf7(?qd z5q@jkc^nA&!K-}-1P=Ry0yyze0W!+h^iW}7jzC1{?|rEFFWbE^Yu7Y}t?jmP-D$f+ zmqFT7nTl0HL|4jwGm7w@a>9 zKD)V~+g~ysmei$OT5}%$&LK8?ib|8aY|>W3;P+0B;=oD=?1rg+PxKcP(d;OEzq1CKA&y#boc51P^ZJPPS)z5 zAZ)dd2$glGQXFj$`XBBJyl2y-aoBA8121JC9&~|_nY>nkmW>TLi%mWdn-^Jks-Jv| zSR*wij;A3Fcy8KsDjQ15?Z9oOj|Qw2;jgJiq>dxG(2I2RE- z$As!#zSFIskebqU2bnoM^N<4VWD2#>!;saPSsY8OaCCQqkCMdje$C?Sp%V}f2~tG5 z0whMYk6tcaABwu*x)ak@n4sMElGPX1_lmv@bgdI2jPdD|2-<~Jf`L`@>Lj7{<-uLQ zE3S_#3e10q-ra=vaDQ42QUY^@edh>tnTtpBiiDVUk5+Po@%RmuTntOlE29I4MeJI?;`7;{3e4Qst#i-RH6s;>e(Sc+ubF2_gwf5Qi%P!aa89fx6^{~A*&B4Q zKTF|Kx^NkiWx=RDhe<{PWXMQ;2)=SC=yZC&mh?T&CvFVz?5cW~ritRjG2?I0Av_cI z)=s!@MXpXbarYm>Kj0wOxl=eFMgSMc?62U#2gM^li@wKPK9^;;0_h7B>F>0>I3P`{ zr^ygPYp~WVm?Qbp6O3*O2)(`y)x>%ZXtztz zMAcwKDr=TCMY!S-MJ8|2MJCVNUBI0BkJV6?(!~W!_dC{TS=eh}t#X+2D>Kp&)ZN~q zvg!ogxUXu^y(P*;Q+y_rDoGeSCYxkaGPldDDx)k;ocJvvGO#1YKoQLHUf2h_pjm&1 zqh&!_KFH03FcJvSdfgUYMp=5EpigZ*8}7N_W%Ms^WSQ4hH`9>3061OEcxmf~TcYn5_oHtscWn zo5!ayj<_fZ)vHu3!A!7M;4y1QIr8YGy$P2qDD_4+T8^=^dB6uNsz|D>p~4pF3Nrb6 zcpRK*($<~JUqOya#M1=#IhOZ zG)W+rJS-x(6EoVz)P zsSo>JtnChdj9^);su%SkFG~_7JPM zEDz3gk2T7Y%x>1tWyia|op(ilEzvAujW?Xwlw>J6d7yEi8E zv30riR|a_MM%ZZX&n!qm0{2agq(s?x9E@=*tyT$nND+{Djpm7Rsy!+c$j+wqMwTOF zZL8BQ|I`<^bGW)5apO{lh(Asqen?_U`$_n0-Ob~Yd%^89oEe%9yGumQ_8Be+l2k+n zCxT%s?bMpv|AdWP7M1LQwLm|x+igA~;+iK-*+tClF&ueX_V}>=4gvZ01xpubQWXD_ zi?Un>&3=$fu)dgk-Z;0Ll}HK5_YM->l^Czrd0^cJ))(DwL2g3aZuza7ga9^|mT_70 z))}A}r1#-(9cxtn<9jGRwOB4hb9kK@YCgjfOM-90I$8@l=H^`K$cyhe2mTM|FY9vW znH~h)I<_aa#V1xmhk?Ng@$Jw-s%a!$BI4Us+Df+?J&gKAF-M`v}j`OWKP3>6`X`tEmhe#y*(Xm$_^Ybbs=%;L7h zp7q^C*qM}Krqsinq|WolR99>_!GL#Z71Hhz|IwQQv<>Ds09B?Je(lhI1(FInO8mc} zl$RyKCUmfku+Cd^8s0|t+e}5g7M{ZPJQH=UB3(~U&(w#Bz#@DTDHy>_UaS~AtN>4O zJ-I#U@R($fgupHebcpuEBX`SZ>kN!rW$#9>s{^3`86ZRQRtYTY)hiFm_9wU3c`SC8 z-5M%g)h}3Pt|wyj#F%}pGC@VL`9&>9P+_UbudCkS%y2w&*o})hBplrB*@Z?gel5q+ z%|*59(sR9GMk3xME}wd%&k?7~J)OL`rK#4d-haC7uaU8-L@?$K6(r<0e<;y83rK&` z3Q!1rD9WkcB8WBQ|WT|$u^lkr0UL4WH4EQTJyk@5gzHb18cOte4w zS`fLv8q;PvAZyY;*Go3Qw1~5#gP0D0ERla6M6#{; zr1l?bR}Nh+OC7)4bfAs(0ZD(axaw6j9v`^jh5>*Eo&$dAnt?c|Y*ckEORIiJXfGcM zEo`bmIq6rJm`XhkXR-^3d8^RTK2;nmVetHfUNugJG(4XLOu>HJA;0EWb~?&|0abr6 zxqVp@p=b3MN^|~?djPe!=eex(u!x>RYFAj|*T$cTi*Sd3Bme7Pri1tkK9N`KtRmXf zZYNBNtik97ct1R^vamQBfo9ZUR@k*LhIg8OR9d_{iv#t)LQV91^5}K5u{eyxwOFoU zHMVq$C>tfa@uNDW^_>EmO~WYQd(@!nKmAvSSIb&hPO|}g-3985t?|R&WZXvxS}Kt2i^eRe>WHb_;-K5cM4=@AN1>E&1c$k!w4O*oscx(f=<1K6l#8Exi)U(ZiZ zdr#YTP6?m1e1dOKysUjQ^>-MR={OuD00g6+(a^cvcmn#A_%Fh3Of%(qP5nvjS1=(> z|Ld8{u%(J}%2SY~+$4pjy{()5HN2MYUjg1X9umxOMFFPdM+IwOVEs4Z(olynvT%G) zt9|#VR}%O2@f6=+6uvbZv{3U)l;C{tuc zZ{K$rut=eS%3_~fQv^@$HV6#9)K9>|0qD$EV2$G^XUNBLM|5-ZmFF!KV)$4l^KVj@ zZ4fI}Knv*K%zPqK77}B-h_V{66VrmoZP2>@^euu8Rc}#qwRwt5uEBWcJJE5*5rT2t zA4Jpx`QQ~1Sh_n_a9x%Il!t1&B~J6p54zxAJx`REov${jeuL8h8x-z=?qwMAmPK5i z_*ES)BW(NZluu#Bmn1-NUKQip_X&_WzJy~J`WYxEJQ&Gu7DD< z&F9urE;}8S{x4{yB zaq~1Zrz%8)<`prSQv$eu5@1RY2WLu=waPTrn`WK%;G5(jt^FeM;gOdvXQjYhax~_> z{bS_`;t#$RYMu-;_Dd&o+LD<5Afg6v{NK?0d8dD5ohAN?QoocETBj?y{MB)jQ%UQ}#t3j&iL!qr@#6JEajR3@^k5wgLfI9S9dT2^f`2wd z%I#Q*@Ctk@w=(u)@QC}yBvUP&fFRR-uYKJ){Wp3&$s(o~W7OzgsUIPx0|ph2L1(r*_Pa@T@mcH^JxBjh09#fgo|W#gG7}|)k&uD1iZxb0 z@|Y)W79SKj9sS&EhmTD;uI#)FE6VwQ*YAr&foK$RI5H8_ripb$^=;U%gWbrrk4!5P zXDcyscEZoSH~n6VJu8$^6LE6)>+=o#Q-~*jmob^@191+Ot1w454e3)WMliLtY6~^w zW|n#R@~{5K#P+(w+XC%(+UcOrk|yzkEes=!qW%imu6>zjdb!B#`efaliKtN}_c!Jp zfyZa`n+Nx8;*AquvMT2;c8fnYszdDA*0(R`bsof1W<#O{v%O!1IO4WZe=>XBu_D%d zOwWDaEtX%@B>4V%f1+dKqcXT>m2!|&?}(GK8e&R=&w?V`*Vj)sCetWp9lr@@{xe6a zE)JL&;p}OnOO}Nw?vFyoccXT*z*?r}E8{uPtd;4<(hmX;d$rqJhEF}I+kD+m(ke;J z7Cm$W*CSdcD=RYEBhedg>tuT{PHqwCdDP*NkHv4rvQTXkzEn*Mb0oJz&+WfWIOS4@ zzpPJ|e%a-PIwOaOC7uQcHQ-q(SE(e@fj+7oC@34wzaBNaP;cw&gm{Z8yYX?V(lIv5 zKbg*zo1m5aGA4^lwJ|bAU=j3*d8S{vp!~fLFcK8s6%Ng55_qW_d*3R%e=34aDZPfD z&Le39j|ahp6E7B0*9OVdeMNrTErFatiE+=Z!XZ^tv0y%zZKXRTBuPyP&C{5(H?t)S zKV24_-TKpOmCPzU&by8R1Q5HY^@IDoeDA9MbgizgQ*F1Er~HVmvSU>vx}pZVQ&tr| zOtZl8vfY2#L<)gZ=ba&wG~EI*Vd?}lRMCf+!b5CDz$8~be-HKMo5omk$w7p4`Mym*IR8WiTz4^kKcUo^8Hkcsu14u z`Pkg`#-Y^A%CqJ0O@UF|caAulf68@(zhqp~YjzInh7qSN7Ov%Aj(Qz%{3zW|xubJ- ztNE_u_MO7Q_585r;xD?e=Er}@U1G@BKW5v$UM((eByhH2p!^g9W}99OD8VV@7d{#H zv)Eam+^K(5>-Ot~U!R$Um3prQmM)7DyK=iM%vy>BRX4#aH7*oCMmz07YB(EL!^%F7?CA#>zXqiYDhS;e?LYPTf(bte6B ztrfvDXYG*T;ExK-w?Knt{jNv)>KMk*sM^ngZ-WiUN;=0Ev^GIDMs=AyLg2V@3R z7ugNc45;4!RPxvzoT}3NCMeK$7j#q3r_xV(@t@OPRyoKBzHJ#IepkDsm$EJRxL)A* zf{_GQYttu^OXr$jHQn}zs$Eh|s|Z!r?Yi+bS-bi+PE*lH zo|6ztu6$r_?|B~S#m>imI!kQP9`6X426uHRri!wGcK;J;`%sFM(D#*Le~W*t2uH`Q z(HEO9-c_`mhA@4QhbW+tgtt9Pzx=_*3Kh~TB$SKmU4yx-Ay&)n%PZPKg#rD4H{%Ke zdMY@rf5EAFfqtrf?Vmk&N(_d-<=bvfOdPrYwY*;5%j@O6@O#Qj7LJTk-x3LN+dEKy+X z>~U8j3Ql`exr1jR>+S4nEy+4c2f{-Q!3_9)yY758tLGg7k^=nt<6h$YE$ltA+13S<}uOg#XHe6 zZHKdNsAnMQ_RIuB;mdoZ%RWpandzLR-BnjN2j@lkBbBd+?i ze*!5mC}!Qj(Q!rTu`KrRRqp22c=hF6<^v&iCDB`n7mHl;vdclcer%;{;=kA(PwdGG zdX#BWoC!leBC4);^J^tPkPbIe<)~nYb6R3u{HvC!NOQa?DC^Q`|_@ zcz;rk`a!4rSLAS>_=b@g?Yab4%=J3Cc7pRv8?_rHMl_aK*HSPU%0pG2Fyhef_biA!aW|-(( z*RIdG&Lmk(=(nk28Q1k1Oa$8Oa-phG%Mc6dT3>JIylcMMIc{&FsBYBD^n@#~>C?HG z*1&FpYVvXOU@~r2(BUa+KZv;tZ15#RewooEM0LFb>guQN;Z0EBFMFMZ=-m$a3;gVD z)2EBD4+*=6ZF?+)P`z@DOT;azK0Q4p4>NfwDR#Pd;no|{q_qB!zk1O8QojE;>zhPu z1Q=1z^0MYHo1*``H3ex|bW-Zy==5J4fE2;g6sq6YcXMYK5i|S^9(OSw#v!3^!EB<% zZF~J~CleS`V-peStyf*I%1^R88D;+8{{qN6-t!@gTARDg^w2`uSzFZbPQ!)q^oC}m zPo8VOQxq2BaIN`pAVFGu8!{p3}(+iZ`f4ck2ygVpEZMQW38nLpj3NQx+&sAkb8`}P3- zc>N*k6AG?r}bfO6_vccTuKX+*- z7W4Q#2``P0jIHYs)F>uG#AM#I6W2)!Nu2nD5{CRV_PmkDS2ditmbd#pggqEgAo%5oC?|CP zGa0CV)wA*ko!xC7pZYkqo{10CN_e00FX5SjWkI3?@XG}}bze!(&+k2$C-C`6temSk z_YyYpB^wh3woo`B zrMSTd4T?(X-jh`FeO76C(3xsOm9s2BP_b%ospg^!#*2*o9N;tf4(X9$qc_d(()yz5 zDk@1}u_Xd+86vy5RBs?LQCuYKCGPS;E4uFOi@V%1JTK&|eRf~lp$AV#;*#O}iRI2=i3rFL8{ zA^ptDZ0l6k-mq=hUJ0x$Y@J>UNfz~I5l63H(`~*v;qX`Z{zwsQQD-!wp0D&hyB8&Z z7$R07gIKGJ^%AvQ{4KM0edM39iFRx=P^6`!<1(s0t|JbB2tXs_B_IH9#ajH0C=-n+ z`nz`fKMBKLlf?2AC+|83M+0rqR%uhNGD;uKA6jOjp7YDe^4%0fRB<^bcjlS2KF~F; zu09wh1x0&4pG&76M;x8$u`b134t=dEPBn6PV|X29<#T4F1mxGF*HOgiWU8tN@cguI z_F@o+XL7FJztR63wC|j4x_DANzcX94r7Iz-O2x$({&qd*mdLG=-Rv)uZ}UlMR+F&q zU}=lkfb0p1>1Ho){o$@}mSKIV;h*$AND7~Dl)QzpFBlSM99Kx+F7GsVK5xcR? z_4Q(Z%cgk8ST}U;;=!LwyZVu^S$>B-Waeik%wzcKTIqeX=0FP(TGQ=nxi=dsS5BYF zl@?}NT!Y!Iyos^@v7XWXA{_bV~1lxz7gC?xuXxy0_?GaN!AhRRM5>)^t%&ODd;@HN5L{MD3 zc>i2keQZVm#?NrDwbfd}_<*5^U&w0zv~n-y8=GGN-!=_`FU^cM8oVCWRFxw?BM^YD zi=Vxz4q|jwPTg+?q7_XI)-S@gQkh>w0ZUB}a{^ z_i;`Y(~fvpI!vmW*A^|P7(6+@C4UeL2WATf{P1?H5rk`5{TL zcf!CgP6Mi{MvjZS)rfo7JLDZK7M7ANd$3`{j9baD*7{#Zu-33fOYUzjvtKzR2)_T1I1s7fe&z|=)QkX;=`zX8!Byw-veM#yr;|wjO^II>!B*B z0+w%;0(=*G3V@88t!}~zx)&do(uF=073Yeh*fEhZb3Vn>t!m(9p~Y_FdV3IgR)9eT z)~e9xpI%2deTWyHlXA(7srrfc_`7ACm!R>SoIgkuF8 z!wkOhrixFy9y@)GdxAntd!!7@=L_tFD2T5OdSUO)I%yj02le`qeQ=yKq$g^h)NG;# za(0J@#VBi^5YI|QI=rq{KlxwGabZJ0dKmfWDROkcM}lUN$@DV`K7fU?8CP2H23QPi zG?YF*=Vn=kTK*#Y_{AQN&oLju|0#E=fx%YVh>S{puu&K$b;BN*jIo@VYhqPiJPzzM>#kxoy0vW9i;ne2_BIG0zyRFp<3M(iY(%*M_>q0ulV2K}Tg zkG{EWKS{i%4DUuHi%DVKy%e+Q!~Uf`>>F6NgD{{I8~nO4!VgOvtFOc7(O)X`|7n*f zxBa4CJ-v9fUUH+`7sPVvpM_C*udZ@OTGTzx56QM5y~OlrZc&w9=)B?nmd@keRn+^= zvm~4sa5987LFDnU{(N|N zJAR8H@}p1fC+H(yTI4n#%~TbImMpuqYn9cQ<0QQ%=PzZItLkC*ef9WJUvfITKWh#D zc#__8`4am9%#NslIUw+<82#SR8AYG|woLfBg#!-&dqq}@P>|I0%lbdy0lSMmNe+}o zj0zZuFr6Wb?Y{Qy-S=|r`bdrDmhnmvkRnkdn`YCleU>Q$=je}LGhh>_QAj6aa_0Oc z%Swsmui;IRx7bN*=AAS@5yW&Y2hy;3&|HAiA8}!HT6!Z!RVn~MZg`RmI6&%#tBZDx zfD+y@Z~NWlk*4l13vmt3AK2wP!fQlnBbECL>?p)F?T)<`w&QN>cP_V>r7UTcsTaaP zTOb$f!P@zf$6>890NVKbIkG8rE?9!Y97sMSZjfF?A zYR8lp`LMoz~O?iaZN;gcX;LC-%Ia*R%A&SLx!YIf29?P+=XAAojK8!^OU*@?R&DK!#G_lsn!#;S375uZ&B0HH1|BO0R90$U>qs zSvHv>H~mAgNCcjo-e+;RjY6B9NCbQrZ|BHjTkehaU<9CSkdd>Vl*ifA2LNOP&R2Qdy3k3-TQ+ zbq=#vI43x`s=%~cGyN&y4Y!FxhwgDe@i6uv8^BLL&3z*SO=D0aLjih?gY4-9uWp5or)H+v~w6n5X#F-I52z=Z_p4JB(;M| zeaVFhuR2|3UD2MzVc~^nSoD2(dD#uL_1PdnIxeA{V5n`#3xf1Zx@4lw(DsQ&H$h zw#%3O<1173hjg2_nhKi!d1ej=h7y`hVjCNB6|HTnx>SWuCE-kgTnfT+YGX4_Lun({ zDv2`>d3vrS)tTf7ps_vvh!Cx^e1BFuWnEAh0(7fkNk|-3oU|iRWdsC6U)?Raft~HN z;^$U}vZK5O8|LV$>6X5T(uYkblv{zwPxnQBh(BQ5tA~J!vGiAMYP^_ki~pkIxDfOZ zUJDwq%O~WueeV6%uN<54&u*c&E4y431cklBNrb06zGOOy4XNT~JS-q(s6@)F@ovbe ze`fial(O4(-su%6@@1+V0MsdLLMyE8;)nou(7}czU(5ASaZYDT(kUZ0L(&g$nF^n9 z9-Pi`ZZLX&)^*M6As4_2Mmc9S7OT)F8KkL2NJ)KJcnCuWU=Wy402A&45#Q9Id~BBH z0cY*xlv!uXzKrXLH!xQu(OtJvEj|0-DmRj1vjFz{c*I4$Pe(+_V|^b~S!0xm{8lq= zZv)@NlcyL3Xdz+*|L137F7y6L-2VsrKw=q^S>F6i%<{Fr8zk06$Ay-(!L$fY@7mcng!2}L0t zgi|KxfB63Xtk_Q8#ZPipQ@!zgjdpEIbK_?q17Hoi4Eiyun$hrc>T(7pOLVLQE=lgGwA+A308p& z7@=09(|$>eLy5gLe{*|3b(M;1n;C^~v?o88jYib48eR4$QGsBFzd}3QuwO^_XE(=B zq+hMi0UFC|dB{LCwch7;zYT=NK})O%sgi0k#yV;My@24^B1+CuZmYOh0^b)5Ba_)) zC%i#_Iev&nsu%I|1N5=MVc#PrlunKAs&hY|3s5;@}`>sB>}gzxuB zB=2vrRyB3uiyW(hkDUNe1@&(b`;>ZvGgw|@s{zVC#_`HXIN_^J@Etb zA7A+F?ot37T{<-vTy8h&b3e+WKHE1oh;pUQrN4yRRrx?mT_9jRa2i4l1fUnLW^Cbl z!I1>VzyFe?VELWWhM?@?t-YPZkD-Qjo@bC2(o#ZtZmr{KZsdFWItV`rs$gp{724@C zL8K5}E0+DHcWcL^{BGei4>@J-3%a#$y6;I}=upc};-NDv-z#kPX26ylOpH)Ov1uU{ zkLj6oiH6l_s+B~_z;|Jc2oi?naS7#3H63~~lWj4rUnd=fCnKdkik<@R&kch9q##G{ z4u!%=rlM~Yp3jk*t8}1B`Sv6<%Z^}~1e@aq zg|JQ`QO2pSjAm-g*?IrNc$^~sIrNBo2$m|Sxanr?Mfs>2@Auu49 zGXlsS<9XS1&8h(dD*Hl&5HBDG!^pJ*lkau_Ur+7`7z;rcs$hT4we?3bT=7Fe<>{5( z2m2(c+hUz2BTHM8dCe*Z3XX&Av;b~a=$6EF>&^E8%nyxO@m_n!q&XD^A{SRjRZQ0L~qDeC=j&0$j6=LNIz@`ni^>ch|sv}^6 zlm>?28yPl@WmDPR?Y-A9X{U9Dv_IsbXJnzKCjkRksLOg#42uG2mE_acbTQ4)J|1V>%U@K(FP3AYhL0U zdeOCPN1qLv!|#c=p!_+%VNV(GHt`RuLRV^vz<5tt-r)yOK**kUWPspVAf|}ZL{LS= z@k(@@!P&W!>wwe`x{+GrFSWhHov7hu?{KuuT%kl#WO@*WX$i_@retlhQBj++SVNCx z5$78LxP>Z=^aJ)D280r_jj=zFfMJFXCIe^B{~V@d1rl_F(qo&AB4bC-vYL>x2jSKX zpuTG-6kgp3e^T&+dtV*i6a~)v@n?n*MffN59y}<0djUX zt27R+SE#hp8bzc#;rk$jw3r4)Q@eI$*`_)=Pvge8@8|8>H3X)<9YX6cXa=ii#Le;(qKm@%0-7$>2ShnYc`j#zJ7gu_FE^?uAkL|H)UIH#gPu^40!6^J=^ zr`}iwa^!4tzW~vOMZAaKF>*8A{^8m$i(VK)>?=#l`xrVe>wseSvM_aF zATNkY>kM_P3?1kE`uIq#mvr-wuTgUH0N<&JhF=(E9%^NS*HLm!4GZ4_XI zL=R5tlG5Mk_1rPfg)sk^llFuKPMPBhuU|L5q#yP_mzxp1o&pAzi-X31sgFpIHn@($ z_>=`AB5(8tP6p2zS5VEvH5J$M` z_much3>S7t3Yo`Yx!>83-hW9LYzDKP?mKdkD#QAK8*M((sx{eBQdrR<^3ZhFP81+& zBnJMUefQyNBji~$5d88Wfw1Lv59aJN9t2!pABLg;ewJ#LXL-10;QcJl+Y4Mtngb)k6JZlCf)3uD_u)J3sYyN;NN5hNbg$%W!i-GK%e&!Us)2IExWSss$YG(hm3kJ-h%yD z>8q^n$+4I(_y_mbT{du4P%h1j3oSpjhY97{+IZ`aA4ug!vNJ6*p?<2H(2w+GD3j$I z1TUXGyNzdf>_yB3grP~FZUs<2Quw;eEi*7s(-MiIkQ%@J^+WGdQvYSUN+TRiD-xto zJ=OUU+kxGYc!HCLNbCvR4lGTp~#L;DFzGd-#gJe*xf(P3hDQz|y)?b9mwU3WUVnpcqXM<@w%r-k*Wr^gzAv)8T^sqA=Ye z!7qy&exJmAcAt~CwS#@yNmjr8*T*!A6w4~E*ibaLRs0CFo(;R3=ODhDt6zWNodmo0 zXx&bT$6&+5c>a|WJ)F4G-^GjY0H#*tY=UNyYr_q5fsrcjk(c^~e*7Lf`!Jd`)p412 zn|^*hV= zFI4UbwA%X@smDd$cQOiMC%jfitTxTb+#`9`G=2rJDfK!E=5ra|So>lc{X1$~w28i+ z4p&cTGwZ#5VueiXS9O8#;RR$yg7tL9!^)Sz&pZYIzlSh}0}V{LxL$Cu%B4U5_}k}- zm~|CsD<076x@<>m=6w6N?WaThIBP`!u{-;WF)xc=2otx*lwf|5+MkdJePjh(B z9SH+%cHGCMAXNxB{_3^otDWdsV7Ob6n{0 z+&!(;iaHOX__5z_$Qk{%xYV%Ig@7iokGBwR`3642ZP#H#v9QGbWl8<|MS*=@qO@Uj z6+SZ_v9`1paUe5tFN~v(b#J3a_Lx0+;r9giZIx-A5TxdbG>xi#AZ5_z1V}B^n)sxT zz49}eK7EWb6wR!6-qQOrHQHkUvshvq%=G2d&@(#XM*Am1;WbnJ{X_!a{ZkphD$^TQ z=Iskb&}=lBm(RHiwJoGg`*NiQ6#RB$T#LF+>#ef;Jne&MxKPX!#r`&TVEFsp2jnNx>dClzpcPy&G&13a_<0qaR3i+k212~hoQ z8nMk{JP-t04I{GW5gUBqcJW-jSMrlw}>p)ptx?WKuCUV77taMiV zHok9V=6yv+Uts@fMY&A}amC=!Yj}eL@=e%XJ#%?agkt1jWF+10{(E9mHLDa>Ll7Vj zG=3cp%ljIB-6pC}6&`xJ*6WCP|IlglLWJ^?yviI8Ve)?V_i4%n;olzny62_`-|IGi z^=}p_O>Z8M;c4|RExu70E7ePW(HWVS&E$+LL6xSQgB`QfMQJ|4pCTFowA39p5P-|$ zUtM_H2HnP8_RoS~Vwk(FhbG zH41licj%=0a;Ln2STFBvU}Ne&O&%8bYKj!h1FA#sNM`232fX|U3QPp#3C?mN2;hE9 z;)!@5ixSPl<89^7gwhHc2YAX1KJK$#*3`KOMIQ253q7-*RJ5k)zp9GBO|Ga~X*^}US5oN@aG&waHV%vi~r{t^`ptTxb zL}q1W8S7*>7oWwvgV4uFLZ(@k`R*=LO_|Gu`prs~!WQXj-NLIa^2(7IHg>BG^N zc|i{-^=&Cek9dkJFQys|sjG9i>LLz|;yCv{^1i%c*h>8zF91kLvS9HBQi~ZU!JL`B zK8N+U0fr1*6??Ium)AF!6tc1eGhXIYL6IRT7rmKp7+>?%5Pa6zC5)KY$ycF0ZJ`G5nEQDG100U-jLkH8^UE4g6wq?sg%pP=-$&G#bcN`^?w3a6 z((s$6eRKcSEIslW-kk5Qi|5Mg-(xdLF}PxxVh$PuO}#aR6pW1kV4Af!Bqh*btXNNZ z>-4(IUl+L4dw+3LcpGut=qB45O+W)Q5?*zZ2A6rJcg`qkSvWA!j^r2mqKuCm6`Py? z@^T#Ux04HemPGd!Hs7NkZdVn1}8_j`o?)*OKZGS!`ff)gF zG?v-lj$wWNWCcw2Mg2o18D~1?3_b0XzdiKBNkYSDpcv@&kp0POmweJE2ZkIQ3B!a! zIgIoE+Xv?;34kyo^QYjZk+tEqZvq^#QG(OzX4~X+KtsoQoddTWUR(yo8R+ObEF1j<-syWOb>)JQ&Zbdu(sctU%Mt zW&YR0{ttY2TTXYZ?~WNU&cES1Z2q(7SrWDh``!J(JM+Nk$!hu&Y;(7E`ZNKTe0w+% zJc?Qnw2B+%UR}0;cB0Rufa(7-3FF}?629@LgTiEC&2uyL6NxexOp?AKT^aAx3gi(W zao>r>MPw0eQ3>IV02uLsC@>yK_epX6GRg4{NEL2wPPF9=*L2RV3yyK8DhuEK>rmmV z`&Q~#c`lgR&93TdOCja|ewOXmPNRh7!&dMT(1ett#iDr8HZW~VqWW@7fe9B6;7S+? zbC`d4@MEau&mKlOPKd>*10q0c{~^baw6!a*w^sY#0Xim{oOsiXiDOhbG&kl3c$$n1 zMRrD83&QucDSEcV*7LIp8VTA@F<%qe+_c`L;6on(>SjAU^}5c9!BCffT>$VQhe=)z z8(=Ej{5>jhmjB3{xDfj2R@VmHQ!CqjlO4KnuOmvHy3K#po$yp_V;p_MKjh1`(rzj6 zHW956k1yvntz{_g?Xbs`avK(IjlTnsu%htO;D7 z?J#x^EzuvVn&NA=!MEj7cwe5A-Z$Zk2LBZH$~%E* zf`((xH0?`}hs|HA%mtwfOEsZJxxrennkTYcwP#FKO5%Lpc^JXhSpV|ZH$Wr;`}`_( zIP==gd3LYyVtwD|*ZJGi{7~x8{=^bGVqu0RJ`n_BZH9+}kz%-4ZRsImi@rx%=ZEKs zcPnUXo6hbJV>fH;@1|bAHIe0ijYI*&kdT|HkDS$9No9 zCHo=*HWb~U+Dtzxr+Esao}6@|;Pf+E$ay0$kQp#s{wlw+7aIKbMdf`OqhoG*;Tco0 zjrP}VQG#Y2cJuqoJg&5({)S(BA}q9T1lGeWRyu=Je|)I!6a+aj!IP^1({)ZYe&x6w zt3a)Dq^TB+A7CdB0-}#z2Ur$W&h3YVw8==!xONy$uQmDWh-@15iEOt!q2m&?ZLA|w z8loSb(0}7y6Xu0?M5Uf4>VZGluB`wMf2oh;m)ghxVda>3m}4%V)r^0nVQ5V6f3>*) z0&VN!N0~GC^P}vj$`EDMZEmVV;N&RISY2C;$0;2(<{Lt&PKzqRByQdiEHGAbwtbS zPj`Da5%U6k1oEtVzI}QNw;!hT6F+~|@=c@$C4NtO@=xgP?|5MyZAyuCzcvq4rdAv@C06%gZ`9%I);R6UGiGJobfux+<0DLS&|MSG4UH z_~o{^^9>ixMg~mY!-@Fai{xaE4^;qy9iZN15Gbn5ZqHWf>Jc5Rv6(#n8`1NcCsdmG zab*dSXVPaE?)wCalD;$ivF%@nB#7D`@YG04p6ed9m}4iJW|pfVMLE<-c{=-8$e?cH zUdU#mCj4gb zZKA^b9p*9S(}8@tw~1RNPHr7tQr;P+-)D8|sq=*o)G%RGqt> zzP5yf`pVxb)I51D_G~Xp^GNK zVI6sAX)a9s)e{8N3?35YA6aQTXuyszK3ah~CemzA&CII#8F&F#KN41~8I^&_%}6MCNb{W87qAF`zj_Y^szhb> z3p3}KbOxotY|(lD=;)`fYE_*{S}x;f^SW#)SU&5X#o|-R|trpa|L5PS5aa0 zTHw8%SDSVtU4?vyrhnq+^@dgFS)|(y{~(4j%3UEiO-rBM9%`)8(dh33pMLiuurNY# z#10AsQ7%*0Cu_DSAU}P;X(JwA64~Q_^R%d_zSm^6Aux?Pn70PM>9EvLeOX z&w9c)pGmcL22;MO3C_B>=NC0RJpMp8?#ZUf=GWRvy z6RHq3B}=MGVg?9@iKFBpsvnkVh3{Vpp=`CcD=u~@ql{my|6?3ssi3mCOPnjI&E}VC zc@X+Yl>;;DNo0W0`0th!X{?luDhOC{E8N=?!w}K1{V=)+1={m(f`Oc|N=07>}3;z{-(A zm{JL=j?Sro5iecmE2-pWlRf(r%|HEQ7kgwQ9+kt=NBhtQI7OwcZ#3%$Uf%^r2nhjY zoQ08MfC%_X{O9~WcirMZMhn#z^ux4Erx-tf-6bHD)9eH&^L>^jvAd^9A^DCDs?0;k zkm7LE*KjP6`2d17MrQaaLqd_Rka}J$csvUec#hw78<=s(hyR>065~YCVCA9+#Q+; za(*L0IEw!r5P|@-;x33L$Lv9 zcuN8YG&g{<(SeJG18~(b!5yywSqQiLAX0;---;}mF5&b4lg|T?LwKREa{9YX_-zL@ZE?Zqi@HxK^2KO1>0LATu{te=T zprmHtY)bDVfxI1S}KBE7V zznP7KQ8HekWU#W6mw`dr-boV}pMQR==&5=Q5T=_q091jfc;R*jX#&=MQ%~@E@9^?`$v48ks<>(fI(F6L(5ppKy|$HWng*bKOb(4|cMUB&z$#ob#XV z5-mg)gmFIybZf=znm3ZPyUO^GJfxt0kmHjaTZ|sthsxXw&}Y)fOUSg=JhRSR^UjZ- zhqqb}Wsyw4zdnj6@#BAJa#-PdI4_dgafFXh85DsEQ_cT+5)XpZq$fZlBA_9UsE9r6 zEFec5?uqN@QhJ^IzwZrwl-5J`CmVPv{(YDTqEqWR^dI;5hXc~cxP%B3v&~s0`Ct89 z@S`i~a^c%V^N81dDT*ItFS*&IN;@O$EgzX0e7x&}TD=!zS}hTpezBLS>mdX(5< z)8DEI(-o_D)c-UX@dA1MuJ*yc>Hf4|`*B2S_O>w*-tbUwtiu`;W(Ud{HTty@(&x(T(F&;M zJ=?H>6`B7nf-90e8V`WSVp|0oEKB-P2M{}4ZDawzvM&a!y>`Y#jCsD%T_l``@ah(I2nJs~Q|%uSKu@k!m~*8B*IoA{*TgtF<(5sHCGG;n@NE%~Xt(G$^&<87u;}Na zx-8cq0g`uA(&RBFo=-4Y1GUZ<``Zw{xL4jfHkZw~%~wvtGueszcXt)_QwH8g!; z%s&3kSa~R$dO$-%L-)c@_hi7&>{6L_M>OZFkUQu;{sL_bUMStNrt{{&O(Wn~*zPOk zB>dnfszb29NSTf2pqIs68k|p-UrSrxgLHqi?3N-UFa!LHy9n1)=s>`yS+J{MEzS@ zNlfGtpma7kG&LR3JE@wB%rFA*h~~KitlO=IP)ZjN6dQLM6qsry zHkB#cyNh#n`)}bCrN1My*;k)^@>e4gJ`LJK?2)Pwp?4Tl4)4FA0(tvY+#1jOUM)xw zlMz4x-f@g^+yKUN`?Vu)|AwujArnM~Pa@y*Q9S8eS(u{-S%(Z5=R~pRl5ZGDjdqH% zC8rW&{##wOpU_oTIG4WXMk4&%2t1;lWcW5&!yxmOT*!hBcKyTqEcNoO+R2;Q?Yj+W z1-Y4?59fijz4(MIDwGe4-baYf08UCs;r|YefD-Md2ST;=cxwpgW=tR76-dQVAhn^= zG9Wk5lQk%jIR@KNU!UMp6@BfU;r+;y4VQ)D2!Il9HX%yW-9nOzV+m$YKzVaO`B8S7t z$!S2Mz`xw>V(RjE`0>bQp<0y&h~Y=M#jpy!#=dE>`=e_AjSZq6u!Dy1xJf~-7|0F! zPR9|n`e_7D2DIV2H(CESQ}hA>U>n|6`%z?YKEA~)BOVY%y=jPV zT=44R!L?J)736X#csn|lfBJ)o8ixaZclguWgrGO<`TN2FMfO}7;5}d+BlK0yTSH3* z4!=;5rOh85&2|x=46hkNaz?)U8&=bcfh=N_#8BNpZ2v$aVBo;sk^*X`v;4-LU;D>! zM*h12MxXIQy)SfAqE4;jY)wgnppazZkdNNVVF;(PLf^qK$FgY9+VFyBKE7UC|f z`R|?&egV11K3s$rJ6!GvoeW=jV*!-e(wA;x(2=d0E_e_%0x--0o8#~m^H1%AH5Z^B zn!TNPn927*bvaf0pt}zhK0o^V@WlGwwKo(*nQ|Q~4_;>~-8y20`HP>@UJa)3nEnGG z5Hwhs|FcmFG16ZVNb5hL`2Gc1{zWIMM{_OiKewV!hCi}U!VuE?s9wU-QbZ!)+Y^tS zGzp5OSi5iq6hmEr$w}&9DFgoB+i*`q`8TBi^MVS{SKEb8Aw%@K7@XCo(De2A`6%mf&a2#~y1N)+kJLD$1HCP!22)(U}xo2|j?WRzt(11j8Z_*v;P$R+Ug*Gy3VxV4K; zGGUGabnW*`Z}~`ydXL-l9e=GC$pY#z|63vy>E*m=$=j}iWP{sRTh0%H54`t>2xYH% zsk+M&u&pNgMCM@3e)Xc?jBWX-TIR_cQ1Z!RW7!B zBjZX=+^3}?SE)B+$EP+0oi1Fp5blDT?*}nsP>filqXH{ms zxU<$hetC`u)Wi+x|EKL-`y^#aQX+sDYIa{M;V%LqLrOk~lR>u0Q!+pyQSU4zY`?E^ z|5@)C)w6G_=i5YYC5SE_u(7hDNYr}uKT|@DSqF%S++lTIbIk^$a>{~0IH8KNFEy%+ zW#$&!ynpgNJh>6uR~?2c)ZMW+h0OKu231(7L_vETPaR+(P)Zy%0~yGm>E9?@@x!Jy z3PYgS}Q@b}x}E#F27@F+j}0=&Ql4gES&f8acMrPAVlVs9$97`FR))R5wI zc&}KFI1UIewh>3PkhnB7u zS3AT8_*|nexznG|Z*DU0c!K@jsI4J)5#DyNi#|e#`l1Vv1`1)*NVcy0LZ``aL0n8B zecupJ(rhq3u8bW0NIRhKYq$v1li+jp*4hfAd&wxYDE8vn1TQ7S@bTM|I2Ob z8vMOIxA7&_j{AKmD+O@EyXT`|dElt0pED^@IV0m)RPBUs*5jW60>>w1!@_G3aBKzG z_f(KfAPBk}-jQtR*Sroq!*3rbQ_m27e+YdzQjUb<_*k8vc_C)y!@cj5E>NxUhPu&g z@Z2<~esU`)ih+4opWe+K7sbN9n*9@n>#@n3*o z?xoROgDuvhq>jJ;Ve{6i<3roQNfgo5^4Q4(|GNExO2Dr7GjgA2zWuKp_K)K0R(6lv z!l$!zW-+T6mb3gQaAFviTQi{|*t%>{(mhTdy+y;Re4qT@kccy#{b z&zWy~kLO@>*WPj2k#H)|7L&gAJ37DmHQAme#@m;(Y8Nu^`D5vf8sZFW#+lA2!HK=( zJ)#hO6JD*`o~&c*&46d}g=Qj@SsoB5ikC z^1V8E+&<-OzuS_C`p5<<(A6fB`LXT(!kV^0_~hL6PpW4={l%|#xgdh?5EIk~lu8{D z2hiyhv3Yxij_#$Wu>P@7SYsl`-~3;}Ktx{34_NL^Kwin&=?!HDv3elQDbcU*qyYpN z(#yw~f1vFGK-t%CC-qa-4FYHbA^h>bag-I&*qaxwn?Qv|idE$<>1H|Gr6JtUu(he2$eg!N z@HTF@dG1)*y;4fxe)4_ZkpaBHH9hXp9p4|gLrRQyuevRd@gSS}JhRnWqrvm|U@>qM z=yl7RQROTKwQtzP3!zUF)_6Ld#NGA6v~2{J9Dd`h6{%+XsU#qGLh%`fB1Hc?wfayK zN`H4BpDp)npVQuu$DVW1qsBS&AJ2eP%6Qw>;k{)Z$8%HL=Q4(a$Ng2_vHw&vA!1L+9zc8vaX2GtqJ{L-;gvF0IR$em zMQ8@{Qp3+3Quk)TJ$?I<8KmwzD*7#(q<@Mc`dchngW}cRG14(Z6K7{T|LhFXwhqUQ;BET;cYqPcAcMgt6M$V9$(?jHo@Sud$an$U&5F zZ1QNh^ztt)E*d#Ij;<43oSKKnd+WNr$_r}+s_O_x6DZSB10*5Q{ourqq>mTl| zx4y^(cy+9;t@R=*j>3_dmm_m)$k$#937V(sllby&5)Xex^UD-|m|q<(jEd#@DV(of zAd7sSdmS*zUDqJ9|K%O2J2OfdUiK{{b{PCy)pi<;hp~7v1CQj&4-10 zgO<3dqhYH1#-Fa}Q{pjql5>>P6gZH21zLfxZ4$SK4T@7b!|`nWF9b*84Bq8&Eht;9 z*P72x&NUCZ7*@B$`FtE=hz5b}S`|c6Ey+j@D1ZibjJaRlR;{cxAWv z?Nqa>QqV*H-*zzaPvpLMHt~nl(x6?vrPpR?zn7~wow?oj*1TKmx4j71>$hvtC$DLD zUrz0^tiP0792U&dxJxNv@r}Elsjn^aSLUu=9#mD{&9n8|ayIL$!H3s>%KEvbchBFW z%cd?VU83mGF#Dar9*s~w&AnmQRQIOvR+uWsuZ?+|a=TzApXO@q^(r%8=}iv#wCnFq z=K9}JbqU@k99Q%j-}NNk+qLCP)jXfmOO|)@?mHcnynd6({mJisP1_}u7k)|eYHXWK z63eQ)E$ufFi!3CWUY2gw%e>omCv}qEX66aH-k&35f9`Q@Us|NPetVqe8=dX*VxJdn ze`q7b=Dn(UA(2sf&g)cOmQFhNJ#<-aMELJZbA#@to>25@kbW<)&!X01 z%NMJt>1ST)tyX)h@?`DxhbgCHr>S4wv}WC&Nw-!{+Z7$2D}74QAcXTvip=M0%Tp_N zor=k`)t|ra^ySr-+(|R9mB(E=`MX#y(wSw)$!iymzB;^c*>%&^*7HxTnRga=soSZT zdDl+9s;r!v8hk6POtzBaig4pRp7eWF(<8gufvNHPu6xs-=e{;mnHzJyGKE+8L0j}; z@%8-e^UCL5HhMiR>sD3Rve&yVZ#{Q1*CO8c+qSr^Z#CN;)(X5>tGG5yUw3<+CfhaL z%bP;hZ?jvgJU67BWyiy74_)6r)_nSxttxn0`0?HE^5(uydHVgP+HE$V?Lv)Leti43 zWA|;f-RqX``95>)^P-fw!Vi{3KNsII-*5f){gdxqd%gVdB1sOBNe=nEW%;i~g_P8J w!5uhoe-Jcg1nPN%MiEAtgE$;km@@t6ukO)1^!cY^83Pb_y85}Sb4q9e0FIsP9{>OV literal 0 HcmV?d00001 diff --git a/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/image_picker/image_picker_macos/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr Date: Sun, 11 Jun 2023 13:14:23 -0400 Subject: [PATCH 16/53] Roll Flutter from da127f15ad54 to 3df163ff081e (25 revisions) (#4183) https://github.com/flutter/flutter/compare/da127f15ad54...3df163ff081e 2023-06-11 engine-flutter-autoroll@skia.org Roll Flutter Engine from 73a7af805472 to 1cca9cc6dbd1 (1 revision) (flutter/flutter#128658) 2023-06-11 engine-flutter-autoroll@skia.org Roll Flutter Engine from 7c6770083e5c to 73a7af805472 (2 revisions) (flutter/flutter#128654) 2023-06-11 engine-flutter-autoroll@skia.org Roll Flutter Engine from 6d615bbcfccf to 7c6770083e5c (2 revisions) (flutter/flutter#128653) 2023-06-11 engine-flutter-autoroll@skia.org Roll Flutter Engine from b19b93de5b0a to 6d615bbcfccf (1 revision) (flutter/flutter#128650) 2023-06-11 engine-flutter-autoroll@skia.org Roll Flutter Engine from 3d76ba6d6d5f to b19b93de5b0a (2 revisions) (flutter/flutter#128649) 2023-06-11 engine-flutter-autoroll@skia.org Roll Flutter Engine from 962d78e0ae9c to 3d76ba6d6d5f (1 revision) (flutter/flutter#128645) 2023-06-10 31859944+LongCatIsLooong@users.noreply.github.com migrate `Tooltip` to use `OverlayPortal` (flutter/flutter#127728) 2023-06-10 engine-flutter-autoroll@skia.org Roll Flutter Engine from b037db26037f to 962d78e0ae9c (10 revisions) (flutter/flutter#128643) 2023-06-10 49699333+dependabot[bot]@users.noreply.github.com Bump actions/checkout from 3.5.2 to 3.5.3 (flutter/flutter#128625) 2023-06-10 engine-flutter-autoroll@skia.org Roll Flutter Engine from 3e90345cdca7 to b037db26037f (1 revision) (flutter/flutter#128627) 2023-06-10 devZhulanov.A.A@gmail.com Remove unnecessary parentheses (flutter/flutter#128620) 2023-06-09 engine-flutter-autoroll@skia.org Roll Flutter Engine from 488876ed26c6 to 3e90345cdca7 (3 revisions) (flutter/flutter#128617) 2023-06-09 andrewrkolos@gmail.com rename generated asset manifest file back to `AssetManifest.bin` (from `AssetManifest.smcbin`) (flutter/flutter#128529) 2023-06-09 jhy03261997@gmail.com Add Selected semantics to IconButton (flutter/flutter#128547) 2023-06-09 engine-flutter-autoroll@skia.org Roll Flutter Engine from 071e1fb21c7a to 488876ed26c6 (5 revisions) (flutter/flutter#128612) 2023-06-09 47866232+chunhtai@users.noreply.github.com Clarifies semantics long press and semantics on tap documentation (flutter/flutter#128599) 2023-06-09 engine-flutter-autoroll@skia.org Roll Flutter Engine from bc6e047570f6 to 071e1fb21c7a (1 revision) (flutter/flutter#128602) 2023-06-09 hans.muller@gmail.com Revert "Update `chip.dart` to use set of `MaterialState`" (flutter/flutter#128607) 2023-06-09 devZhulanov.A.A@gmail.com Add tooltips for `SegmentedButton` (flutter/flutter#128501) 2023-06-09 jason-simmons@users.noreply.github.com Ignore app.stop events received before the app.detach response in attach integration tests (flutter/flutter#128593) 2023-06-09 dleyba042@gmail.com ExpansionPanel isExpanded callback parameter (Ticket 74114) (flutter/flutter#128082) 2023-06-09 engine-flutter-autoroll@skia.org Roll Flutter Engine from 93afba901b3b to bc6e047570f6 (3 revisions) (flutter/flutter#128594) 2023-06-09 hans.muller@gmail.com Updated flutter_localizations tests for Material3; (flutter/flutter#128521) 2023-06-09 jhy03261997@gmail.com Paint SelectableFragments before text (flutter/flutter#128375) 2023-06-09 engine-flutter-autoroll@skia.org Roll Packages from e13b8c43386a to afe2f05c1adb (7 revisions) (flutter/flutter#128582) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC rmistry@google.com,stuartmorgan@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index dd97cf9799a3..573a40b4ff41 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -da127f15ad54f3396e475540dbfb3fda790a0e1d +3df163ff081e967ce52c9cc9d5bbda9d27bb1414 From 762fd24e4fd354fc05ab476271e1a6fdd4909ebd Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Mon, 12 Jun 2023 13:01:06 -0400 Subject: [PATCH 17/53] Roll Flutter from 3df163ff081e to 353b8bc87d29 (10 revisions) (#4198) https://github.com/flutter/flutter/compare/3df163ff081e...353b8bc87d29 2023-06-12 engine-flutter-autoroll@skia.org Roll Packages from afe2f05c1adb to c9865e8c63dc (6 revisions) (flutter/flutter#128701) 2023-06-12 nt4f04uNd@gmail.com Fix RangeSlider notifies start and end twice when participates in gesture arena (flutter/flutter#128674) 2023-06-12 polinach@google.com Address leak tracker breaking changes. (flutter/flutter#128623) 2023-06-12 tessertaha@gmail.com Update `ListTile` text defaults to use `ColorScheme` (flutter/flutter#128581) 2023-06-12 engine-flutter-autoroll@skia.org Roll Flutter Engine from effea50196ca to 4b022f4e871f (2 revisions) (flutter/flutter#128683) 2023-06-12 engine-flutter-autoroll@skia.org Roll Flutter Engine from 9bb0a5907403 to effea50196ca (1 revision) (flutter/flutter#128678) 2023-06-11 engine-flutter-autoroll@skia.org Roll Flutter Engine from 788437e41ee0 to 9bb0a5907403 (1 revision) (flutter/flutter#128673) 2023-06-11 engine-flutter-autoroll@skia.org Roll Flutter Engine from 5da44b9aafdd to 788437e41ee0 (2 revisions) (flutter/flutter#128672) 2023-06-11 engine-flutter-autoroll@skia.org Roll Flutter Engine from 6e2c71adccad to 5da44b9aafdd (1 revision) (flutter/flutter#128669) 2023-06-11 engine-flutter-autoroll@skia.org Roll Flutter Engine from 1cca9cc6dbd1 to 6e2c71adccad (1 revision) (flutter/flutter#128662) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC rmistry@google.com,stuartmorgan@google.com,tarrinneal@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 573a40b4ff41..f9066d59f22b 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -3df163ff081e967ce52c9cc9d5bbda9d27bb1414 +353b8bc87d297295069ae1b47885920f07d16e9e From e71868e40567e632697d492f2e2d476726a26b2b Mon Sep 17 00:00:00 2001 From: Hashir Shoaib Date: Mon, 12 Jun 2023 22:56:07 +0500 Subject: [PATCH 18/53] [go_router] Fixes a bug in `debugLogDiagnostics` to support StatefulShellRoute (#4177) PR fixes the following issue: - https://github.com/flutter/flutter/issues/127957 --- packages/go_router/CHANGELOG.md | 4 +++ packages/go_router/lib/src/configuration.dart | 2 +- packages/go_router/pubspec.yaml | 2 +- .../go_router/test/configuration_test.dart | 36 ++++++++++++++++++- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 173973a67e91..28e8c8d12acf 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 8.0.2 + +- Fixes a bug in `debugLogDiagnostics` to support StatefulShellRoute. + ## 8.0.1 - Fixes a link for an example in `path` documentation. diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index f2e9d84617d8..bd10649d1112 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -552,7 +552,7 @@ class RouteConfiguration { final String fullPath = concatenatePaths(parentFullpath, route.path); sb.writeln(' => ${''.padLeft(depth * 2)}$fullPath'); _debugFullPathsFor(route.routes, fullPath, depth + 1, sb); - } else if (route is ShellRoute) { + } else if (route is ShellRouteBase) { _debugFullPathsFor(route.routes, parentFullpath, depth, sb); } } diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 2d152daa8ae1..f5cd92323bf8 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 8.0.1 +version: 8.0.2 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 diff --git a/packages/go_router/test/configuration_test.dart b/packages/go_router/test/configuration_test.dart index ac6ff0e398d8..ac4d1ed848fa 100644 --- a/packages/go_router/test/configuration_test.dart +++ b/packages/go_router/test/configuration_test.dart @@ -1037,6 +1037,33 @@ void main() { ), ], ), + GoRoute( + path: '/g', + builder: _mockScreenBuilder, + routes: [ + StatefulShellRoute.indexedStack( + builder: _mockIndexedStackShellBuilder, + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: 'h', + builder: _mockScreenBuilder, + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: 'i', + builder: _mockScreenBuilder, + ), + ], + ), + ], + ), + ], + ), ], redirectLimit: 10, topRedirect: (BuildContext context, GoRouterState state) { @@ -1049,7 +1076,10 @@ void main() { ' => /a/c\n' ' => /d\n' ' => /d/e\n' - ' => /d/e/f\n', + ' => /d/e/f\n' + ' => /g\n' + ' => /g/h\n' + ' => /g/i\n', ); }, ); @@ -1069,3 +1099,7 @@ Widget _mockScreenBuilder(BuildContext context, GoRouterState state) => Widget _mockShellBuilder( BuildContext context, GoRouterState state, Widget child) => child; + +Widget _mockIndexedStackShellBuilder(BuildContext context, GoRouterState state, + StatefulNavigationShell shell) => + shell; From 64a10c13928913aa590dc813d9a3ed83eab87b1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 17:56:10 +0000 Subject: [PATCH 19/53] [sign_in]: Bump com.google.guava:guava from 32.0.0-android to 32.0.1-android in /packages/google_sign_in/google_sign_in_android/android (#4184) Bumps [com.google.guava:guava](https://github.com/google/guava) from 32.0.0-android to 32.0.1-android.
Release notes

Sourced from com.google.guava:guava's releases.

32.0.1

Maven

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>32.0.1-jre</version>
  <!-- or, for Android: -->
  <version>32.0.1-android</version>
</dependency>

Jar files

Guava requires one runtime dependency, which you can download here:

Javadoc

JDiff

Changelog

  • io: Fixed Files.createTempDir and FileBackedOutputStream under Windows, which broke as part of the security fix in release 32.0.0. Sorry for the trouble. (fdbf77d3f2)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.google.guava:guava&package-manager=gradle&previous-version=32.0.0-android&new-version=32.0.1-android)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- packages/google_sign_in/google_sign_in_android/CHANGELOG.md | 4 ++++ .../google_sign_in_android/android/build.gradle | 2 +- packages/google_sign_in/google_sign_in_android/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md index e31a3c1d99c4..2d9aae4ff784 100644 --- a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.1.16 + +* Updates Guava to version 32.0.1. + ## 6.1.15 * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. diff --git a/packages/google_sign_in/google_sign_in_android/android/build.gradle b/packages/google_sign_in/google_sign_in_android/android/build.gradle index 8c2566852df5..388821458158 100644 --- a/packages/google_sign_in/google_sign_in_android/android/build.gradle +++ b/packages/google_sign_in/google_sign_in_android/android/build.gradle @@ -60,7 +60,7 @@ android { dependencies { implementation 'com.google.android.gms:play-services-auth:20.5.0' - implementation 'com.google.guava:guava:32.0.0-android' + implementation 'com.google.guava:guava:32.0.1-android' testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:5.0.0' } diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml index db3fd5f8d716..046baa1e8c6f 100644 --- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in_android description: Android implementation of the google_sign_in plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 6.1.15 +version: 6.1.16 environment: sdk: ">=2.18.0 <4.0.0" From b6798e985c026345f118242f973c7fc529cd6df5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 18:22:40 +0000 Subject: [PATCH 20/53] [local_auth]: Bump androidx.fragment:fragment from 1.5.7 to 1.6.0 in /packages/local_auth/local_auth_android/android (#4186) Bumps androidx.fragment:fragment from 1.5.7 to 1.6.0. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=androidx.fragment:fragment&package-manager=gradle&previous-version=1.5.7&new-version=1.6.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- packages/local_auth/local_auth_android/CHANGELOG.md | 3 ++- packages/local_auth/local_auth_android/android/build.gradle | 2 +- packages/local_auth/local_auth_android/pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index d34206069d6d..ed2b24d16c74 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -1,7 +1,8 @@ -## NEXT +## 1.0.32 * Fixes stale ignore: prefer_const_constructors. * Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. +* Updates androidx.fragment version to 1.6.0. ## 1.0.31 diff --git a/packages/local_auth/local_auth_android/android/build.gradle b/packages/local_auth/local_auth_android/android/build.gradle index 0d7223191644..b55df15f3c9b 100644 --- a/packages/local_auth/local_auth_android/android/build.gradle +++ b/packages/local_auth/local_auth_android/android/build.gradle @@ -61,7 +61,7 @@ android { dependencies { api "androidx.core:core:1.10.1" api "androidx.biometric:biometric:1.1.0" - api "androidx.fragment:fragment:1.5.7" + api "androidx.fragment:fragment:1.6.0" testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'org.robolectric:robolectric:4.10.3' diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index 4a3e4f110350..4c492fd3c6cf 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_android description: Android implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.31 +version: 1.0.32 environment: sdk: ">=3.0.0 <4.0.0" From 2d573fe913ebddeacb4ea869461b3b419aa48b5b Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Mon, 12 Jun 2023 14:49:35 -0400 Subject: [PATCH 21/53] [ci] Finish migrating Pigeon tests to LUCI (#3192) Completes the migration of macOS CI tests to LUCI, since the Pigeon test issues in CI seems to be resolved now (likely due to the Xcode updates in CI). Fixes https://github.com/flutter/flutter/issues/120231 --- .cirrus.yml | 28 ---------------------------- packages/pigeon/tool/run_tests.dart | 16 +++------------- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index c8a7af48cce9..47ee7ff760b8 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -22,13 +22,6 @@ tool_setup_template: &TOOL_SETUP_TEMPLATE tool_setup_script: - .ci/scripts/prepare_tool.sh -macos_template: &MACOS_TEMPLATE - # Only one macOS task can run in parallel without credits, so use them for - # PRs on macOS. - use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' - macos_instance: - image: ghcr.io/cirruslabs/macos-ventura-xcode:14.3 - flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE upgrade_flutter_script: # Channels that are part of our normal test matrix use a pinned, @@ -335,24 +328,3 @@ task: - cd ../.. - flutter packages get - dart testing/web_benchmarks_test.dart - -# macOS tasks. -task: - << : *FLUTTER_UPGRADE_TEMPLATE - << : *MACOS_TEMPLATE - matrix: - - name: macos-custom_package_tests - env: - PATH: $PATH:/usr/local/bin - matrix: - CHANNEL: "master" - CHANNEL: "stable" - # Create an iPhone 13, to match what is available on LUCI, since Pigeon tests - # currently have a hard-coded device. - create_simulator_script: - - xcrun simctl list - - xcrun simctl create "iPhone 13" com.apple.CoreSimulator.SimDeviceType.iPhone-13 com.apple.CoreSimulator.SimRuntime.iOS-16-4 - local_tests_script: - # script/configs/linux_only_custom_test.yaml - # Custom tests need Chrome for these packages. (They run in linux-custom_package_tests) - - ./script/tool_runner.sh custom-test --exclude=script/configs/linux_only_custom_test.yaml diff --git a/packages/pigeon/tool/run_tests.dart b/packages/pigeon/tool/run_tests.dart index 250e18c4cef7..fd00ecf0fac1 100644 --- a/packages/pigeon/tool/run_tests.dart +++ b/packages/pigeon/tool/run_tests.dart @@ -119,10 +119,7 @@ Future main(List args) async { // androidJavaIntegrationTests, // androidKotlinIntegrationTests, ]; - // Run macOS and iOS tests on macOS, since that's the only place they can run. - // TODO(stuartmorgan): Move everything to LUCI, and eliminate the LUCI/Cirrus - // separation. See https://github.com/flutter/flutter/issues/120231. - const List macOSHostLuciTests = [ + const List macOSHostTests = [ iOSObjCUnitTests, // TODO(stuartmorgan): Enable by default once CI issues are solved; see // https://github.com/flutter/packages/pull/2816. @@ -132,8 +129,6 @@ Future main(List args) async { // should be enabled if any iOS-only tests are added (e.g., for a feature // not supported by macOS). // iOSSwiftIntegrationTests, - ]; - const List macOSHostCirrusTests = [ iOSSwiftUnitTests, macOSSwiftUnitTests, macOSSwiftIntegrationTests, @@ -146,8 +141,7 @@ Future main(List args) async { _validateTestCoverage(>[ linuxHostTests, - macOSHostLuciTests, - macOSHostCirrusTests, + macOSHostTests, windowsHostTests, // Tests that are deliberately not included in CI: [ @@ -177,11 +171,7 @@ Future main(List args) async { final List testsToRun; if (Platform.isMacOS) { - if (Platform.environment['LUCI_CI'] != null) { - testsToRun = macOSHostLuciTests; - } else { - testsToRun = macOSHostCirrusTests; - } + testsToRun = macOSHostTests; } else if (Platform.isWindows) { testsToRun = windowsHostTests; } else if (Platform.isLinux) { From 050729760b251e315af01876cc7d7de5dcfba0e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 19:43:55 +0000 Subject: [PATCH 22/53] [camera]: Bump com.google.guava:guava from 32.0.0-android to 32.0.1-android in /packages/camera/camera_android_camerax/android (#4195) Bumps [com.google.guava:guava](https://github.com/google/guava) from 32.0.0-android to 32.0.1-android.
Release notes

Sourced from com.google.guava:guava's releases.

32.0.1

Maven

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>32.0.1-jre</version>
  <!-- or, for Android: -->
  <version>32.0.1-android</version>
</dependency>

Jar files

Guava requires one runtime dependency, which you can download here:

Javadoc

JDiff

Changelog

  • io: Fixed Files.createTempDir and FileBackedOutputStream under Windows, which broke as part of the security fix in release 32.0.0. Sorry for the trouble. (fdbf77d3f2)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.google.guava:guava&package-manager=gradle&previous-version=32.0.0-android&new-version=32.0.1-android)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- packages/camera/camera_android_camerax/CHANGELOG.md | 4 ++++ packages/camera/camera_android_camerax/android/build.gradle | 2 +- packages/camera/camera_android_camerax/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 5912f9834548..2980f18c19bf 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.0+7 + +* Updates Guava version to 32.0.1. + ## 0.5.0+6 * Updates Guava version to 32.0.0. diff --git a/packages/camera/camera_android_camerax/android/build.gradle b/packages/camera/camera_android_camerax/android/build.gradle index 83871babe062..f10f13336081 100644 --- a/packages/camera/camera_android_camerax/android/build.gradle +++ b/packages/camera/camera_android_camerax/android/build.gradle @@ -66,7 +66,7 @@ dependencies { implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}" implementation "androidx.camera:camera-video:${camerax_version}" - implementation 'com.google.guava:guava:32.0.0-android' + implementation 'com.google.guava:guava:32.0.1-android' testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'androidx.test:core:1.4.0' diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 5e4ea92ba6c5..d0364186b281 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.5.0+6 +version: 0.5.0+7 environment: sdk: ">=2.19.0 <4.0.0" From 1dad75928f90ffba271e8f993cb25d168d38a3e0 Mon Sep 17 00:00:00 2001 From: Khalid Muhammad <62033170+Khalidm98@users.noreply.github.com> Date: Tue, 13 Jun 2023 18:46:22 +0300 Subject: [PATCH 23/53] [go_router] #127016 preserved route name case when caching `_nameToPath` (#4196) preserved route name case when caching `_nameToPath` by not using `toLowerCase()` Related issue: [#127016](https://github.com/flutter/flutter/issues/127016) --- packages/go_router/CHANGELOG.md | 4 ++ packages/go_router/lib/src/configuration.dart | 7 +- packages/go_router/pubspec.yaml | 2 +- packages/go_router/test/go_router_test.dart | 14 ++-- packages/go_router/test/name_case_test.dart | 66 +++++++++++++++++++ packages/go_router/test/parser_test.dart | 3 - 6 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 packages/go_router/test/name_case_test.dart diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 28e8c8d12acf..49764fbf9571 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 8.0.3 + +- Makes namedLocation and route name related APIs case sensitive. + ## 8.0.2 - Fixes a bug in `debugLogDiagnostics` to support StatefulShellRoute. diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index bd10649d1112..89fb5400de67 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -221,9 +221,8 @@ class RouteConfiguration { '${queryParameters.isEmpty ? '' : ', queryParameters: $queryParameters'}'); return true; }()); - final String keyName = name.toLowerCase(); - assert(_nameToPath.containsKey(keyName), 'unknown route name: $name'); - final String path = _nameToPath[keyName]!; + assert(_nameToPath.containsKey(name), 'unknown route name: $name'); + final String path = _nameToPath[name]!; assert(() { // Check that all required params are present final List paramNames = []; @@ -564,7 +563,7 @@ class RouteConfiguration { final String fullPath = concatenatePaths(parentFullPath, route.path); if (route.name != null) { - final String name = route.name!.toLowerCase(); + final String name = route.name!; assert( !_nameToPath.containsKey(name), 'duplication fullpaths for name ' diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index f5cd92323bf8..a2f48a79a434 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 8.0.2 +version: 8.0.3 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index b3b46660e64d..ed258aa7e9b4 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -1493,8 +1493,7 @@ void main() { }, throwsA(isAssertionError)); }); - testWidgets('match case insensitive w/ params', - (WidgetTester tester) async { + testWidgets('cannot match case insensitive', (WidgetTester tester) async { final List routes = [ GoRoute( name: 'home', @@ -1524,8 +1523,15 @@ void main() { ]; final GoRouter router = await createRouter(routes, tester); - router.goNamed('person', - pathParameters: {'fid': 'f2', 'pid': 'p1'}); + expect( + () { + router.goNamed( + 'person', + pathParameters: {'fid': 'f2', 'pid': 'p1'}, + ); + }, + throwsAssertionError, + ); }); testWidgets('too few params', (WidgetTester tester) async { diff --git a/packages/go_router/test/name_case_test.dart b/packages/go_router/test/name_case_test.dart new file mode 100644 index 000000000000..6e3f067197fe --- /dev/null +++ b/packages/go_router/test/name_case_test.dart @@ -0,0 +1,66 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; + +void main() { + testWidgets( + 'Route names are case sensitive', + (WidgetTester tester) async { + // config router with 2 routes with the same name but different case (Name, name) + final GoRouter router = GoRouter( + routes: [ + GoRoute( + path: '/', + name: 'Name', + builder: (_, __) => const ScreenA(), + ), + GoRoute( + path: '/path', + name: 'name', + builder: (_, __) => const ScreenB(), + ), + ], + ); + + // run MaterialApp, initial screen path is '/' -> ScreenA + await tester.pumpWidget( + MaterialApp.router( + routerConfig: router, + title: 'GoRouter Testcase', + ), + ); + + // go to ScreenB + router.goNamed('name'); + await tester.pumpAndSettle(); + expect(find.byType(ScreenB), findsOneWidget); + + // go to ScreenA + router.goNamed('Name'); + await tester.pumpAndSettle(); + expect(find.byType(ScreenA), findsOneWidget); + }, + ); +} + +class ScreenA extends StatelessWidget { + const ScreenA({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} + +class ScreenB extends StatelessWidget { + const ScreenB({super.key}); + + @override + Widget build(BuildContext context) { + return Container(); + } +} diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart index ee4279b51576..253790f01163 100644 --- a/packages/go_router/test/parser_test.dart +++ b/packages/go_router/test/parser_test.dart @@ -118,11 +118,8 @@ void main() { ); expect(configuration.namedLocation('lowercase'), '/abc'); - expect(configuration.namedLocation('LOWERCASE'), '/abc'); expect(configuration.namedLocation('camelCase'), '/efg'); - expect(configuration.namedLocation('camelcase'), '/efg'); expect(configuration.namedLocation('snake_case'), '/hij'); - expect(configuration.namedLocation('SNAKE_CASE'), '/hij'); // With query parameters expect(configuration.namedLocation('lowercase'), '/abc'); From eed9b682843ba2baff0cf15528e50770f1737a12 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 13 Jun 2023 13:24:56 -0400 Subject: [PATCH 24/53] [tool] Allow running from anywhere (#4199) Now that the repo tooling is always run from source, not via `pub global`, we no longer need to infer the repo location from the current directory. Instead, hard-code knowledge of where the repository root is. This makes it much easier to run the tooling, since it's common to be in a package directory rather than the repo root. To make it even easier to run from within a package, this also adds a `--current-package` as an alternative to `--packages`. This makes it possible to, e.g., write local wrapper scripts that run a specific set of commands on whatever the current package happens to be (such as a generic version of the script discussed in https://github.com/flutter/packages/pull/4129). As related cleanup, makes the tool non-publishable (we haven't been publishing it since the repo merge, but I never made it unpublishable; this is important now that it would not work if published) and remove the LICENSE and CHANGELOG since it's no longer a stand-alone package. Fixes https://github.com/flutter/flutter/issues/128231 Fixes https://github.com/flutter/flutter/issues/128232 --- script/tool/CHANGELOG.md | 675 ------------------ script/tool/LICENSE | 25 - script/tool/README.md | 21 +- .../tool/lib/src/common/package_command.dart | 65 +- script/tool/lib/src/main.dart | 15 +- script/tool/pubspec.yaml | 3 +- .../test/common/package_command_test.dart | 112 ++- 7 files changed, 182 insertions(+), 734 deletions(-) delete mode 100644 script/tool/CHANGELOG.md delete mode 100644 script/tool/LICENSE diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md deleted file mode 100644 index 44d1d72473e7..000000000000 --- a/script/tool/CHANGELOG.md +++ /dev/null @@ -1,675 +0,0 @@ -## 0.13.4+4 - -* Allows code excerpts in `example/README.md`. - -## 0.13.4+3 - -* Moves source to flutter/packages. - -## 0.13.4+2 - -* Expands the `--packages-for-branch` detection of main to include ancestors - of `origin/main` and `upstream/main`. - -## 0.13.4+1 - -* Makes `--packages-for-branch` detect any commit on `main` as being `main`, - so that it works with pinned checkouts (e.g., on LUCI). - -## 0.13.4 - -* Adds the ability to validate minimum supported Dart/Flutter versions in - `pubspec-check`. - -## 0.13.3 - -* Renames `podspecs` to `podspec-check`. The old name will continue to work. -* Adds validation of the Swift-in-Obj-C-projects workaround in the podspecs of - iOS plugin implementations that use Swift. - -## 0.13.2+1 - -* Replaces deprecated `flutter format` with `dart format` in `format` - implementation. - -## 0.13.2 - -* Falls back to other executables in PATH when `clang-format` does not run. - -## 0.13.1 - -* Updates `version-check` to recognize Pigeon's platform test structure. -* Pins `package:git` dependency to `2.0.x` until `dart >=2.18.0` becomes our - oldest legacy. -* Updates test mocks. - -## 0.13.0 - -* Renames `all-plugins-app` to `create-all-packages-app` to clarify what it - actually does. Also renames the project directory it creates from - `all_plugins` to `all_packages`. - -## 0.12.1 - -* Modifies `publish_check_command.dart` to do a `dart pub get` in all examples - of the package being checked. Workaround for [dart-lang/pub#3618](https://github.com/dart-lang/pub/issues/3618). - -## 0.12.0 - -* Changes the behavior of `--packages-for-branch` on main/master to run for - packages changed in the last commit, rather than running for all packages. - This allows CI to test the same filtered set of packages in post-submit as are - tested in presubmit. -* Adds a `fix` command to run `dart fix --apply` in target packages. - -## 0.11.0 - -* Renames `publish-plugin` to `publish`. -* Renames arguments to `list`: - * `--package` now lists top-level packages (previously `--plugin`). - * `--package-or-subpackage` now lists top-level packages (previously - `--package`). - -## 0.10.0+1 - -* Recognizes `run_test.sh` as a developer-only file in `version-check`. -* Adds `readme-check` validation that the example/README.md for a federated - plugin's implementation packages has a warning about the intended use of the - example instead of the template boilerplate. - -## 0.10.0 - -* Improves the logic in `version-check` to determine what changes don't require - version changes, as well as making any dev-only changes also not require - changelog changes since in practice we almost always override the check in - that case. -* Removes special-case handling of Dependabot PRs, and the (fragile) - `--change-description-file` flag was only still used for that case, as - the improved diff analysis now handles that case more robustly. - -## 0.9.3 - -* Raises minimum `compileSdkVersion` to 32 for the `all-plugins-app` command. - -## 0.9.2 - -* Adds checking of `code-excerpt` configuration to `readme-check`, to validate - that if the excerpting tags are added to a README they are actually being - used. - -## 0.9.1 - -* Adds a `--downgrade` flag to `analyze` for analyzing with the oldest possible - versions of packages. - -## 0.9.0 - -* Replaces PR-description-based version/changelog/breaking change check - overrides in `version-check` with label-based overrides using a new - `pr-labels` flag, since we don't actually have reliable access to the - PR description in checks. - -## 0.8.10 - -- Adds a new `remove-dev-dependencies` command to remove `dev_dependencies` - entries to make legacy version analysis possible in more cases. -- Adds a `--lib-only` option to `analyze` to allow only analyzing the client - parts of a library for legacy verison compatibility. - -## 0.8.9 - -- Includes `dev_dependencies` when overridding dependencies using - `make-deps-path-based`. -- Bypasses version and CHANGELOG checks for Dependabot PRs for packages - that are known not to be client-affecting. - -## 0.8.8 - -- Allows pre-release versions in `version-check`. - -## 0.8.7 - -- Supports empty custom analysis allow list files. -- `drive-examples` now validates files to ensure that they don't accidentally - use `test(...)`. -- Adds a new `dependabot-check` command to ensure complete Dependabot coverage. -- Adds `skip-if-not-supporting-dart-version` to allow for the same use cases - as `skip-if-not-supporting-flutter-version` but for packages without Flutter - constraints. - -## 0.8.6 - -- Adds `update-release-info` to apply changelog and optional version changes - across multiple packages. -- Fixes changelog validation when reverting to a `NEXT` state. -- Fixes multiplication of `--force` flag when publishing multiple packages. -- Adds minimum deployment target flags to `xcode-analyze` to allow - enforcing deprecation warning handling in advance of actually dropping - support for an OS version. -- Checks for template boilerplate in `readme-check`. -- `readme-check` now validates example READMEs when present. - -## 0.8.5 - -- Updates `test` to inculde the Dart unit tests of examples, if any. -- `drive-examples` now supports non-plugin packages. -- Commands that iterate over examples now include non-Flutter example packages. - -## 0.8.4 - -- `readme-check` now validates that there's a info tag on code blocks to - identify (and for supported languages, syntax highlight) the language. -- `readme-check` now has a `--require-excerpts` flag to require that any Dart - code blocks be managed by `code_excerpter`. - -## 0.8.3 - -- Adds a new `update-excerpts` command to maintain README files using the - `code-excerpter` package from flutter/site-shared. -- `license-check` now ignores submodules. -- Allows `make-deps-path-based` to skip packages it has alredy rewritten, so - that running multiple times won't fail after the first time. -- Removes UWP support, since Flutter has dropped support for UWP. - -## 0.8.2+1 - -- Adds a new `readme-check` command. -- Updates `publish-plugin` command documentation. -- Fixes `all-plugins-app` to preserve the original application's Dart SDK - version to avoid changing language feature opt-ins that the template may - rely on. -- Fixes `custom-test` to run `pub get` before running Dart test scripts. - -## 0.8.2 - -- Adds a new `custom-test` command. -- Switches from deprecated `flutter packages` alias to `flutter pub`. - -## 0.8.1 - -- Fixes an `analyze` regression in 0.8.0 with packages that have non-`example` - sub-packages. - -## 0.8.0 - -- Ensures that `firebase-test-lab` runs include an `integration_test` runner. -- Adds a `make-deps-path-based` command to convert inter-repo package - dependencies to path-based dependencies. -- Adds a (hidden) `--run-on-dirty-packages` flag for use with - `make-deps-path-based` in CI. -- `--packages` now allows using a federated plugin's package as a target without - fully specifying it (if it is not the same as the plugin's name). E.g., - `--packages=path_provide_ios` now works. -- `--run-on-changed-packages` now includes only the changed packages in a - federated plugin, not all packages in that plugin. -- Fixes `federation-safety-check` handling of plugin deletion, and of top-level - files in unfederated plugins whose names match federated plugin heuristics - (e.g., `packages/foo/foo_android.iml`). -- Adds an auto-retry for failed Firebase Test Lab tests as a short-term patch - for flake issues. -- Adds support for `CHROME_EXECUTABLE` in `drive-examples` to match similar - `flutter` behavior. -- Validates `default_package` entries in plugins. -- Removes `allow-warnings` from the `podspecs` command. -- Adds `skip-if-not-supporting-flutter-version` to allow running tests using a - version of Flutter that not all packages support. (E.g., to allow for running - some tests against old versions of Flutter to help avoid accidental breakage.) - -## 0.7.3 - -- `native-test` now builds unit tests before running them on Windows and Linux, - matching the behavior of other platforms. -- Adds `--log-timing` to add timing information to package headers in looping - commands. -- Adds a `--check-for-missing-changes` flag to `version-check` that requires - version updates (except for recognized exemptions) and CHANGELOG changes when - modifying packages, unless the PR description explains why it's not needed. - -## 0.7.2 - -- Update Firebase Testlab deprecated test device. (Pixel 4 API 29 -> Pixel 5 API 30). -- `native-test --android`, `--ios`, and `--macos` now fail plugins that don't - have unit tests, rather than skipping them. -- Added a new `federation-safety-check` command to help catch changes to - federated packages that have been done in such a way that they will pass in - CI, but fail once the change is landed and published. -- `publish-check` now validates that there is an `AUTHORS` file. -- Added flags to `version-check` to allow overriding the platform interface - major version change restriction. -- Improved error handling and error messages in CHANGELOG version checks. -- `license-check` now validates Kotlin files. -- `pubspec-check` now checks that the description is of the pub-recommended - length. -- Fix `license-check` when run on Windows with line ending conversion enabled. -- Fixed `pubspec-check` on Windows. -- Add support for `main` as a primary branch. `master` continues to work for - compatibility. - -## 0.7.1 - -- Add support for `.pluginToolsConfig.yaml` in the `build-examples` command. - -## 0.7.0 - -- `native-test` now supports `--linux` for unit tests. -- Formatting now skips Dart files that contain a line that exactly - matches the string `// This file is hand-formatted.`. - -## 0.6.0+1 - -- Fixed `build-examples` to work for non-plugin packages. - -## 0.6.0 - -- Added Android native integration test support to `native-test`. -- Added a new `android-lint` command to lint Android plugin native code. -- Pubspec validation now checks for `implements` in implementation packages. -- Pubspec valitation now checks the full relative path of `repository` entries. -- `build-examples` now supports UWP plugins via a `--winuwp` flag. -- `native-test` now supports `--windows` for unit tests. -- **Breaking change**: `publish` no longer accepts `--no-tag-release` or - `--no-push-flags`. Releases now always tag and push. -- **Breaking change**: `publish`'s `--package` flag has been replaced with the - `--packages` flag used by most other packages. -- **Breaking change** Passing both `--run-on-changed-packages` and `--packages` - is now an error; previously it the former would be ignored. - -## 0.5.0 - -- `--exclude` and `--custom-analysis` now accept paths to YAML files that - contain lists of packages to exclude, in addition to just package names, - so that exclude lists can be maintained separately from scripts and CI - configuration. -- Added an `xctest` flag to select specific test targets, to allow running only - unit tests or integration tests. -- **Breaking change**: Split Xcode analysis out of `xctest` and into a new - `xcode-analyze` command. -- Fixed a bug that caused `firebase-test-lab` to hang if it tried to run more - than one plugin's tests in a single run. -- **Breaking change**: If `firebase-test-lab` is run on a package that supports - Android, but for which no tests are run, it now fails instead of skipping. - This matches `drive-examples`, as this command is what is used for driving - Android Flutter integration tests on CI. -- **Breaking change**: Replaced `xctest` with a new `native-test` command that - will eventually be able to run native unit and integration tests for all - platforms. - - Adds the ability to disable test types via `--no-unit` or - `--no-integration`. -- **Breaking change**: Replaced `java-test` with Android unit test support for - the new `native-test` command. -- Commands that print a run summary at the end now track and log exclusions - similarly to skips for easier auditing. -- `version-check` now validates that `NEXT` is not present when changing - the version. - -## 0.4.1 - -- Improved `license-check` output. -- Use `java -version` rather than `java --version`, for compatibility with more - versions of Java. - -## 0.4.0 - -- Modified the output format of many commands -- **Breaking change**: `firebase-test-lab` no longer supports `*_e2e.dart` - files, only `integration_test/*_test.dart`. -- Add a summary to the end of successful command runs for commands using the - new output format. -- Fixed some cases where a failure in a command for a single package would - immediately abort the test. -- Deprecated `--plugins` in favor of new `--packages`. `--plugins` continues to - work for now, but will be removed in the future. -- Make `drive-examples` device detection robust against Flutter tool banners. -- `format` is now supported on Windows. - -## 0.3.0 - -- Add a --build-id flag to `firebase-test-lab` instead of hard-coding the use of - `CIRRUS_BUILD_ID`. `CIRRUS_BUILD_ID` is the default value for that flag, for backward - compatibility. -- `xctest` now supports running macOS tests in addition to iOS - - **Breaking change**: it now requires an `--ios` and/or `--macos` flag. -- **Breaking change**: `build-examples` for iOS now uses `--ios` rather than - `--ipa`. -- The tooling now runs in strong null-safe mode. -- `publish plugins` check against pub.dev to determine if a release should happen. -- Modified the output format of many commands -- Removed `podspec`'s `--skip` in favor of `--ignore` using the new structure. - -## 0.2.0 - -- Remove `xctest`'s `--skip`, which is redundant with `--ignore`. - -## 0.1.4 - -- Add a `pubspec-check` command - -## 0.1.3 - -- Cosmetic fix to `publish-check` output -- Add a --dart-sdk option to `analyze` -- Allow reverts in `version-check` - -## 0.1.2 - -- Add `against-pub` flag for version-check, which allows the command to check version with pub. -- Add `machine` flag for publish-check, which replaces outputs to something parsable by machines. -- Add `skip-conformation` flag to publish-plugin to allow auto publishing. -- Change `run-on-changed-packages` to consider all packages as changed if any - files have been changed that could affect the entire repository. - -## 0.1.1 - -- Update the allowed third-party licenses for flutter/packages. - -## 0.1.0+1 - -- Re-add the bin/ directory. - -## 0.1.0 - -- **NOTE**: This is no longer intended as a general-purpose package, and is now - supported only for flutter/plugins and flutter/tools. -- Fix version checks - - Remove handling of pre-release null-safe versions -- Fix build all for null-safe template apps -- Improve handling of web integration tests -- Supports enforcing standardized copyright files -- Improve handling of iOS tests - -## v.0.0.45+3 - -- Pin `collection` to `1.14.13` to be able to target Flutter stable (v1.22.6). - -## v.0.0.45+2 - -- Make `publish-plugin` to work on non-flutter packages. - -## v.0.0.45+1 - -- Don't call `flutter format` if there are no Dart files to format. - -## v.0.0.45 - -- Add exclude flag to exclude any plugin from further processing. - -## v.0.0.44+7 - -- `all-plugins-app` doesn't override the AGP version. - -## v.0.0.44+6 - -- Fix code formatting. - -## v.0.0.44+5 - -- Remove `-v` flag on drive-examples. - -## v.0.0.44+4 - -- Fix bug where directory isn't passed - -## v.0.0.44+3 - -- More verbose logging - -## v.0.0.44+2 - -- Remove pre-alpha Windows workaround to create examples on the fly. - -## v.0.0.44+1 - -- Print packages that passed tests in `xctest` command. -- Remove printing the whole list of simulators. - -## v.0.0.44 - -- Add 'xctest' command to run xctests. - -## v.0.0.43 - -- Allow minor `*-nullsafety` pre release packages. - -## v.0.0.42+1 - -- Fix test command when `--enable-experiment` is called. - -## v.0.0.42 - -- Allow `*-nullsafety` pre release packages. - -## v.0.0.41 - -- Support `--enable-experiment` flag in subcommands `test`, `build-examples`, `drive-examples`, -and `firebase-test-lab`. - -## v.0.0.40 - -- Support `integration_test/` directory for `drive-examples` command - -## v.0.0.39 - -- Support `integration_test/` directory for `package:integration_test` - -## v.0.0.38 - -- Add C++ and ObjC++ to clang-format. - -## v.0.0.37+2 - -- Make `http` and `http_multi_server` dependency version constraint more flexible. - -## v.0.0.37+1 - -- All_plugin test puts the plugin dependencies into dependency_overrides. - -## v.0.0.37 - -- Only builds mobile example apps when necessary. - -## v.0.0.36+3 - -- Add support for Linux plugins. - -## v.0.0.36+2 - -- Default to showing podspec lint warnings - -## v.0.0.36+1 - -- Serialize linting podspecs. - -## v.0.0.36 - -- Remove retry on Firebase Test Lab's call to gcloud set. -- Remove quiet flag from Firebase Test Lab's gcloud set command. -- Allow Firebase Test Lab command to continue past gcloud set network failures. - This is a mitigation for the network service sometimes not responding, - but it isn't actually necessary to have a network connection for this command. - -## v.0.0.35+1 - -- Minor cleanup to the analyze test. - -## v.0.0.35 - -- Firebase Test Lab command generates a configurable unique path suffix for results. - -## v.0.0.34 - -- Firebase Test Lab command now only tries to configure the project once -- Firebase Test Lab command now retries project configuration up to five times. - -## v.0.0.33+1 - -- Fixes formatting issues that got past our CI due to - https://github.com/flutter/flutter/issues/51585. -- Changes the default package name for testing method `createFakePubspec` back - its previous behavior. - -## v.0.0.33 - -- Version check command now fails on breaking changes to platform interfaces. -- Updated version check test to be more flexible. - -## v.0.0.32+7 - -- Ensure that Firebase Test Lab tests have a unique storage bucket for each test run. - -## v.0.0.32+6 - -- Ensure that Firebase Test Lab tests have a unique storage bucket for each package. - -## v.0.0.32+5 - -- Remove --fail-fast and --silent from lint podspec command. - -## v.0.0.32+4 - -- Update `publish-plugin` to use `flutter pub publish` instead of just `pub - publish`. Enforces a `pub publish` command that matches the Dart SDK in the - user's Flutter install. - -## v.0.0.32+3 - -- Update Firebase Testlab deprecated test device. (Pixel 3 API 28 -> Pixel 4 API 29). - -## v.0.0.32+2 - -- Runs pub get before building macos to avoid failures. - -## v.0.0.32+1 - -- Default macOS example builds to false. Previously they were running whenever - CI was itself running on macOS. - -## v.0.0.32 - -- `analyze` now asserts that the global `analysis_options.yaml` is the only one - by default. Individual directories can be excluded from this check with the - new `--custom-analysis` flag. - -## v.0.0.31+1 - -- Add --skip and --no-analyze flags to podspec command. - -## v.0.0.31 - -- Add support for macos on `DriveExamplesCommand` and `BuildExamplesCommand`. - -## v.0.0.30 - -- Adopt pedantic analysis options, fix firebase_test_lab_test. - -## v.0.0.29 - -- Add a command to run pod lib lint on podspec files. - -## v.0.0.28 - -- Increase Firebase test lab timeouts to 5 minutes. - -## v.0.0.27 - -- Run tests with `--platform=chrome` for web plugins. - -## v.0.0.26 - -- Add a command for publishing plugins to pub. - -## v.0.0.25 - -- Update `DriveExamplesCommand` to use `ProcessRunner`. -- Make `DriveExamplesCommand` rely on `ProcessRunner` to determine if the test fails or not. -- Add simple tests for `DriveExamplesCommand`. - -## v.0.0.24 - -- Gracefully handle pubspec.yaml files for new plugins. -- Additional unit testing. - -## v.0.0.23 - -- Add a test case for transitive dependency solving in the - `create_all_plugins_app` command. - -## v.0.0.22 - -- Updated firebase-test-lab command with updated conventions for test locations. -- Updated firebase-test-lab to add an optional "device" argument. -- Updated version-check command to always compare refs instead of using the working copy. -- Added unit tests for the firebase-test-lab and version-check commands. -- Add ProcessRunner to mock running processes for testing. - -## v.0.0.21 - -- Support the `--plugins` argument for federated plugins. - -## v.0.0.20 - -- Support for finding federated plugins, where one directory contains - multiple packages for different platform implementations. - -## v.0.0.19+3 - -- Use `package:file` for file I/O. - -## v.0.0.19+2 - -- Use java as language when calling `flutter create`. - -## v.0.0.19+1 - -- Rename command for `CreateAllPluginsAppCommand`. - -## v.0.0.19 - -- Use flutter create to build app testing plugin compilation. - -## v.0.0.18+2 - -- Fix `.travis.yml` file name in `README.md`. - -## v0.0.18+1 - -- Skip version check if it contains `publish_to: none`. - -## v0.0.18 - -- Add option to exclude packages from generated pubspec command. - -## v0.0.17+4 - -- Avoid trying to version-check pubspecs that are missing a version. - -## v0.0.17+3 - -- version-check accounts for [pre-1.0 patch versions](https://github.com/flutter/flutter/issues/35412). - -## v0.0.17+2 - -- Fix exception handling for version checker - -## v0.0.17+1 - -- Fix bug where we used a flag instead of an option - -## v0.0.17 - -- Add a command for checking the version number - -## v0.0.16 - -- Add a command for generating `pubspec.yaml` for All Plugins app. - -## v0.0.15 - -- Add a command for running driver tests of plugin examples. - -## v0.0.14 - -- Check for dependencies->flutter instead of top level flutter node. - -## v0.0.13 - -- Differentiate between Flutter and non-Flutter (but potentially Flutter consumed) Dart packages. diff --git a/script/tool/LICENSE b/script/tool/LICENSE deleted file mode 100644 index c6823b81eb84..000000000000 --- a/script/tool/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/script/tool/README.md b/script/tool/README.md index bffa2b66ee9d..435394d80752 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -3,12 +3,8 @@ This is a set of utilities used in this repository, both for CI and for local development. -The tool is designed to be run at the root of the repository or `/packages/`. - ## Getting Started -In flutter/packages, the tool is run from source. - Set up: ```sh @@ -42,31 +38,36 @@ command is targetting. An package name can be any of: - A combination federated_plugin_name/package_name (e.g., `path_provider/path_provider` for the app-facing package). +The examples below assume they are being run from the repository root, but +the script works from anywhere. If you develop in flutter/packages frequently, +it may be useful to make an alias for +`dart run /absolute/path/to/script/tool/bin/flutter_plugin_tools.dart` so that +you can easily run commands from within packages. For that use case there is +also a `--current-package` flag as an alternative to `--packages`, to target the +current working directory's package (or enclosing package; it can be used from +anywhere within a package). + ### Format Code ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart format --packages package_name ``` ### Run the Dart Static Analyzer ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart analyze --packages package_name ``` ### Run Dart Unit Tests ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart test --packages package_name ``` ### Run Dart Integration Tests ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart build-examples --apk --packages package_name dart run script/tool/bin/flutter_plugin_tools.dart drive-examples --android --packages package_name ``` @@ -83,7 +84,6 @@ runs both unit tests and (on platforms that support it) integration tests, but Examples: ```sh -cd # Run just unit tests for iOS and Android: dart run script/tool/bin/flutter_plugin_tools.dart native-test --ios --android --no-integration --packages package_name # Run all tests for macOS: @@ -99,7 +99,6 @@ with submodules, you will need to `git submodule update --init --recursive` before running this command. ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart update-excerpts --packages package_name ``` @@ -114,7 +113,6 @@ For instance, if you add a new analysis option that requires production code changes across many packages: ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart update-release-info \ --version=minimal \ --base-branch=upstream/main \ @@ -143,7 +141,6 @@ For instance, to updated to version 3.0.0 of `some_package` in every package that depends on it: ```sh -cd dart run script/tool/bin/flutter_plugin_tools.dart update-dependency \ --pub-package=some_package \ --version=3.0.0 \ diff --git a/script/tool/lib/src/common/package_command.dart b/script/tool/lib/src/common/package_command.dart index 9b0006123c47..00a38e7045a1 100644 --- a/script/tool/lib/src/common/package_command.dart +++ b/script/tool/lib/src/common/package_command.dart @@ -71,6 +71,7 @@ abstract class PackageCommand extends Command { defaultsTo: [], ); argParser.addFlag(_runOnChangedPackagesArg, + negatable: false, help: 'Run the command on changed packages.\n' 'If no packages have changed, or if there have been changes that may\n' 'affect all packages, the command runs on all packages.\n' @@ -78,18 +79,29 @@ abstract class PackageCommand extends Command { 'See $_baseShaArg if a custom base is needed to determine the diff.\n\n' 'Cannot be combined with $_packagesArg.\n'); argParser.addFlag(_runOnDirtyPackagesArg, + negatable: false, help: 'Run the command on packages with changes that have not been committed.\n' 'Packages excluded with $_excludeArg are excluded even if changed.\n' 'Cannot be combined with $_packagesArg.\n', hide: true); argParser.addFlag(_packagesForBranchArg, + negatable: false, help: 'This runs on all packages changed in the last commit on main ' '(or master), and behaves like --run-on-changed-packages on ' 'any other branch.\n\n' 'Cannot be combined with $_packagesArg.\n\n' 'This is intended for use in CI.\n', hide: true); + argParser.addFlag(_currentPackageArg, + negatable: false, + help: + 'Set the target package(s) based on the current working directory.\n' + '- If the current working directory is (or is inside) a package, ' + 'that package will be targeted.\n' + '- If the current working directory is the root of a federated ' + 'plugin group, that group will be targeted.\n' + 'Cannot be combined with $_packagesArg.\n'); argParser.addOption(_baseShaArg, help: 'The base sha used to determine git diff. \n' 'This is useful when $_runOnChangedPackagesArg is specified.\n' @@ -104,17 +116,22 @@ abstract class PackageCommand extends Command { 'but more information may be added in the future.'); } - static const String _baseBranchArg = 'base-branch'; - static const String _baseShaArg = 'base-sha'; - static const String _excludeArg = 'exclude'; - static const String _logTimingArg = 'log-timing'; + // Package selection. static const String _packagesArg = 'packages'; static const String _packagesForBranchArg = 'packages-for-branch'; + static const String _currentPackageArg = 'current-package'; static const String _pluginsLegacyAliasArg = 'plugins'; static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; static const String _runOnDirtyPackagesArg = 'run-on-dirty-packages'; + static const String _excludeArg = 'exclude'; + // Diff base selection. + static const String _baseBranchArg = 'base-branch'; + static const String _baseShaArg = 'base-sha'; + // Sharding. static const String _shardCountArg = 'shardCount'; static const String _shardIndexArg = 'shardIndex'; + // Utility. + static const String _logTimingArg = 'log-timing'; /// The directory containing the packages. final Directory packagesDir; @@ -308,13 +325,15 @@ abstract class PackageCommand extends Command { _runOnChangedPackagesArg, _runOnDirtyPackagesArg, _packagesForBranchArg, + _currentPackageArg, }; if (packageSelectionFlags .where((String flag) => argResults!.wasParsed(flag)) .length > 1) { - printError('Only one of --$_packagesArg, --$_runOnChangedPackagesArg, or ' - '--$_packagesForBranchArg can be provided.'); + printError('Only one of the package selection arguments ' + '(${packageSelectionFlags.join(", ")}) ' + 'can be provided.'); throw ToolExit(exitInvalidArguments); } @@ -383,6 +402,14 @@ abstract class PackageCommand extends Command { if (packages.isEmpty) { return; } + } else if (getBoolArg(_currentPackageArg)) { + final String? currentPackageName = _getCurrentDirectoryPackageName(); + if (currentPackageName == null) { + printError('Unable to determine packages; --$_currentPackageArg can ' + 'only be used within a repository package or package group.'); + throw ToolExit(exitInvalidArguments); + } + packages = {currentPackageName}; } final Directory thirdPartyPackagesDirectory = packagesDir.parent @@ -545,6 +572,32 @@ abstract class PackageCommand extends Command { return packages; } + String? _getCurrentDirectoryPackageName() { + // Ensure that the current directory is within the packages directory. + final Directory absolutePackagesDir = packagesDir.absolute; + Directory currentDir = packagesDir.fileSystem.currentDirectory.absolute; + if (!currentDir.path.startsWith(absolutePackagesDir.path) || + currentDir.path == packagesDir.path) { + return null; + } + // If the current directory is a direct subdirectory of the packages + // directory, then that's the target. + if (currentDir.parent.path == absolutePackagesDir.path) { + return currentDir.basename; + } + // Otherwise, walk up until a package is found... + while (!isPackage(currentDir)) { + currentDir = currentDir.parent; + if (currentDir.path == absolutePackagesDir.path) { + return null; + } + } + // ... and then check whether it has an enclosing package. + final RepositoryPackage package = RepositoryPackage(currentDir); + final RepositoryPackage? enclosingPackage = package.getEnclosingPackage(); + return (enclosingPackage ?? package).directory.basename; + } + // Returns true if the current checkout is on an ancestor of [branch]. // // This is used because CI may check out a specific hash rather than a branch, diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 1839524a85e9..7c1cbef36948 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -41,17 +41,14 @@ import 'xcode_analyze_command.dart'; void main(List args) { const FileSystem fileSystem = LocalFileSystem(); - - Directory packagesDir = - fileSystem.currentDirectory.childDirectory('packages'); + final Directory scriptBinDir = + fileSystem.file(io.Platform.script.toFilePath()).parent; + final Directory root = scriptBinDir.parent.parent.parent; + final Directory packagesDir = root.childDirectory('packages'); if (!packagesDir.existsSync()) { - if (fileSystem.currentDirectory.basename == 'packages') { - packagesDir = fileSystem.currentDirectory; - } else { - print('Error: Cannot find a "packages" sub-directory'); - io.exit(1); - } + print('Error: Cannot find a "packages" sub-directory'); + io.exit(1); } final CommandRunner commandRunner = CommandRunner( diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 44babd7f0f4b..925d4b60697a 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,8 @@ name: flutter_plugin_tools description: Productivity and CI utils for flutter/packages repository: https://github.com/flutter/packages/tree/main/script/tool -version: 0.13.4+4 +version: 1.0.0 +publish_to: none dependencies: args: ^2.1.0 diff --git a/script/tool/test/common/package_command_test.dart b/script/tool/test/common/package_command_test.dart index 24100bafede3..2ef9b9e8438e 100644 --- a/script/tool/test/common/package_command_test.dart +++ b/script/tool/test/common/package_command_test.dart @@ -319,8 +319,7 @@ packages/plugin1/plugin1/plugin1.dart expect( output, containsAllInOrder([ - contains('Only one of --packages, --run-on-changed-packages, or ' - '--packages-for-branch can be provided.') + contains('Only one of the package selection arguments') ])); }); @@ -338,8 +337,25 @@ packages/plugin1/plugin1/plugin1.dart expect( output, containsAllInOrder([ - contains('Only one of --packages, --run-on-changed-packages, or ' - '--packages-for-branch can be provided.') + contains('Only one of the package selection arguments') + ])); + }); + + test('does not allow --packages with --current-package', () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--current-package', + '--packages=plugin1', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Only one of the package selection arguments') ])); }); @@ -359,10 +375,94 @@ packages/plugin1/plugin1/plugin1.dart expect( output, containsAllInOrder([ - contains('Only one of --packages, --run-on-changed-packages, or ' - '--packages-for-branch can be provided.') + contains('Only one of the package selection arguments') + ])); + }); + }); + + group('current-package', () { + test('throws when run from outside of the packages directory', () async { + fileSystem.currentDirectory = packagesDir.parent; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--current-package', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('--current-package can only be used within a repository ' + 'package or package group') ])); }); + + test('throws when run directly in the packages directory', () async { + fileSystem.currentDirectory = packagesDir; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--current-package', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('--current-package can only be used within a repository ' + 'package or package group') + ])); + }); + + test('runs on a package when run from the package directory', () async { + final RepositoryPackage package = + createFakePlugin('a_package', packagesDir); + createFakePlugin('another_package', packagesDir); + fileSystem.currentDirectory = package.directory; + + await runCapturingPrint( + runner, ['sample', '--current-package']); + + expect(command.plugins, unorderedEquals([package.path])); + }); + + test('runs on a package when run from a package example directory', + () async { + final RepositoryPackage package = createFakePlugin( + 'a_package', packagesDir, + examples: ['a', 'b', 'c']); + createFakePlugin('another_package', packagesDir); + fileSystem.currentDirectory = package.getExamples().first.directory; + + await runCapturingPrint( + runner, ['sample', '--current-package']); + + expect(command.plugins, unorderedEquals([package.path])); + }); + + test('runs on a package group when run from the group directory', + () async { + final Directory pluginGroup = packagesDir.childDirectory('a_plugin'); + final RepositoryPackage plugin1 = + createFakePlugin('a_plugin_foo', pluginGroup); + final RepositoryPackage plugin2 = + createFakePlugin('a_plugin_bar', pluginGroup); + createFakePlugin('unrelated_plugin', packagesDir); + fileSystem.currentDirectory = pluginGroup; + + await runCapturingPrint( + runner, ['sample', '--current-package']); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + }); }); group('test run-on-changed-packages', () { From f9314a3b833aa5bd5eceadebb27f28f97388ef94 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 13 Jun 2023 22:07:53 -0400 Subject: [PATCH 25/53] [tool] Support running main.dart (#4208) The change to how the repository is located assumed the script was being run from bin/flutter_plugin_tools.dart, but it can also be run directly from lib/src/main.dart which was broken. This restores the ability to run it either way. Fixes the tree breakage in `release`. --- script/tool/lib/src/main.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 7c1cbef36948..c86629d4f4a0 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -41,9 +41,13 @@ import 'xcode_analyze_command.dart'; void main(List args) { const FileSystem fileSystem = LocalFileSystem(); - final Directory scriptBinDir = + final Directory scriptDir = fileSystem.file(io.Platform.script.toFilePath()).parent; - final Directory root = scriptBinDir.parent.parent.parent; + // Support running either via directly invoking main.dart, or the wrapper in + // bin/. + final Directory toolsDir = + scriptDir.basename == 'bin' ? scriptDir.parent : scriptDir.parent.parent; + final Directory root = toolsDir.parent.parent; final Directory packagesDir = root.childDirectory('packages'); if (!packagesDir.existsSync()) { From 47273f44da12d9cb81abbfbf65cdade83db4ee58 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 14 Jun 2023 12:53:08 -0400 Subject: [PATCH 26/53] Roll Flutter from 353b8bc87d29 to 09b7e5615761 (17 revisions) (#4206) https://github.com/flutter/flutter/compare/353b8bc87d29...09b7e5615761 2023-06-13 engine-flutter-autoroll@skia.org Roll Packages from c9865e8c63dc to 050729760b25 (6 revisions) (flutter/flutter#128796) 2023-06-13 engine-flutter-autoroll@skia.org Roll Flutter Engine from 346f051ac062 to f9a0a0dafeea (1 revision) (flutter/flutter#128786) 2023-06-13 engine-flutter-autoroll@skia.org Roll Flutter Engine from 5951f90cbac3 to 346f051ac062 (1 revision) (flutter/flutter#128783) 2023-06-13 engine-flutter-autoroll@skia.org Roll Flutter Engine from 3b70103f53bc to 5951f90cbac3 (1 revision) (flutter/flutter#128781) 2023-06-13 engine-flutter-autoroll@skia.org Roll Flutter Engine from 04ffeb4ab103 to 3b70103f53bc (1 revision) (flutter/flutter#128778) 2023-06-13 engine-flutter-autoroll@skia.org Roll Flutter Engine from 42ccd12a14c2 to 04ffeb4ab103 (4 revisions) (flutter/flutter#128772) 2023-06-13 engine-flutter-autoroll@skia.org Roll Flutter Engine from aa1693f6aaeb to 42ccd12a14c2 (1 revision) (flutter/flutter#128761) 2023-06-13 engine-flutter-autoroll@skia.org Roll Flutter Engine from d02b15ef34ef to aa1693f6aaeb (3 revisions) (flutter/flutter#128759) 2023-06-13 engine-flutter-autoroll@skia.org Roll Flutter Engine from f67ed35b142e to d02b15ef34ef (2 revisions) (flutter/flutter#128754) 2023-06-13 engine-flutter-autoroll@skia.org Roll Flutter Engine from 1714d73e681b to f67ed35b142e (3 revisions) (flutter/flutter#128751) 2023-06-12 engine-flutter-autoroll@skia.org Roll Flutter Engine from 12def739b1f6 to 1714d73e681b (2 revisions) (flutter/flutter#128738) 2023-06-12 katelovett@google.com Fix paint offset in reverse for 2D (flutter/flutter#128724) 2023-06-12 jonahwilliams@google.com [flutter_tools] pass through enable impeller flag to macOS. (flutter/flutter#128720) 2023-06-12 engine-flutter-autoroll@skia.org Roll Flutter Engine from de68fba09338 to 12def739b1f6 (2 revisions) (flutter/flutter#128726) 2023-06-12 engine-flutter-autoroll@skia.org Roll Flutter Engine from 33e06934daed to de68fba09338 (10 revisions) (flutter/flutter#128721) 2023-06-12 engine-flutter-autoroll@skia.org Roll Flutter Engine from 4b022f4e871f to 33e06934daed (6 revisions) (flutter/flutter#128706) 2023-06-12 hans.muller@gmail.com Update button tests for Material 3 by default (flutter/flutter#128628) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC rmistry@google.com,stuartmorgan@google.com,tarrinneal@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index f9066d59f22b..55284013ffad 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -353b8bc87d297295069ae1b47885920f07d16e9e +09b7e5615761fa6e678c9951cd6de91e68724e43 From 84e03ffb072cb34c2872423b31b7e72070f93075 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 14 Jun 2023 14:54:47 -0400 Subject: [PATCH 27/53] Roll Flutter (stable) from 682aa387cfe4 to 796c8ef79279 (5 revisions) (#4213) https://github.com/flutter/flutter/compare/682aa387cfe4...796c8ef79279 If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-stable-packages Please CC rmistry@google.com,stuartmorgan@google.com,tarrinneal@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter (stable): https://github.com/flutter/flutter/issues/new/choose To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- .ci/flutter_stable.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_stable.version b/.ci/flutter_stable.version index ec36783b4ac1..f99b2a7b9d5e 100644 --- a/.ci/flutter_stable.version +++ b/.ci/flutter_stable.version @@ -1 +1 @@ -682aa387cfe4fbd71ccd5418b2c2a075729a1c66 +796c8ef79279f9c774545b3771238c3098dbefab From c34ca5db3711bfd24504c3497e7bca1011ef4cf9 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Wed, 14 Jun 2023 14:56:17 -0400 Subject: [PATCH 28/53] Manual roll Flutter from 09b7e5615761 to 95be76ab7e3d (14 revisions) (#4214) Manual roll requested by tarrinneal@google.com https://github.com/flutter/flutter/compare/09b7e5615761...95be76ab7e3d 2023-06-14 mdebbar@google.com [web] Migrate framework away from dart:html and package:js (flutter/flutter#128580) 2023-06-14 77465135+cruiser-baxter@users.noreply.github.com Fixed slider value indicator not disappearing after a bit on desktop platform when slider is clicked not dragged (flutter/flutter#128137) 2023-06-14 goderbauer@google.com Inline AbstractNode into SemanticsNode and Layer (flutter/flutter#128467) 2023-06-14 engine-flutter-autoroll@skia.org Roll Flutter Engine from 727b4413fe6f to 2d8d5ecfe4a8 (5 revisions) (flutter/flutter#128842) 2023-06-14 engine-flutter-autoroll@skia.org Roll Flutter Engine from 66a5761412f9 to 727b4413fe6f (10 revisions) (flutter/flutter#128841) 2023-06-14 engine-flutter-autoroll@skia.org Roll Flutter Engine from b6bf3a6f1ccd to 66a5761412f9 (1 revision) (flutter/flutter#128813) 2023-06-13 william.oprandi+github@gmail.com Fix syntax error in docstring (flutter/flutter#128692) 2023-06-13 36861262+QuncCccccc@users.noreply.github.com Update unit tests in material library for Material 3 (flutter/flutter#128725) 2023-06-13 christopherfujino@gmail.com [flutter_tools] Suppress git output in flutter channel (flutter/flutter#128475) 2023-06-13 katelovett@google.com Fix ensureVisible and default focus traversal for reversed scrollables (flutter/flutter#128756) 2023-06-13 engine-flutter-autoroll@skia.org Roll Flutter Engine from f9a0a0dafeea to b6bf3a6f1ccd (2 revisions) (flutter/flutter#128797) 2023-06-13 36861262+QuncCccccc@users.noreply.github.com Update rest of the unit tests in material library for Material 3 (flutter/flutter#128747) 2023-06-13 36861262+QuncCccccc@users.noreply.github.com Update tests in material library for Material 3 by default (flutter/flutter#128300) 2023-06-13 hans.muller@gmail.com Update misc tests for Material3 (flutter/flutter#128712) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC rmistry@google.com,stuartmorgan@google.com,tarrinneal@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 55284013ffad..0fe2e13c4e26 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -09b7e5615761fa6e678c9951cd6de91e68724e43 +95be76ab7e3dca2def54454313e97f94f4ac4582 From b7f1ad4b3395c410920e7e1d39d7cabc25a2eb72 Mon Sep 17 00:00:00 2001 From: Ahmed Fwela <63286031+ahmednfwela@users.noreply.github.com> Date: Thu, 15 Jun 2023 00:33:10 +0300 Subject: [PATCH 29/53] [flutter_adaptive_scaffold] Support RTL (#4204) This PR removes the hardcoded rtl directionality placed on top of `AdaptiveScaffold` for an unknown reason. | RTL | LTR | |-----|-----| | ![image](https://github.com/flutter/packages/assets/63286031/10adac59-3f9a-4f4d-85da-939efbb47ae8) | ![image](https://github.com/flutter/packages/assets/63286031/c8c1af75-4787-473c-a397-a64d2f9b1e88) | ## Changes in this PR - Core: Removed this hardcoded `Directionality` widget https://github.com/flutter/packages/blob/050729760b251e315af01876cc7d7de5dcfba0e9/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart#L491-L494 - Core: Added tests to ensure text direction is passed correctly - Core: Modified `example/adaptive_scaffold_demo.dart` to demonstrate support for RTL - Side: There were some tests that were getting skipped, I have enabled them since their related issues landed on stable. - Side: Stopped ignoring `prefer_const_constructors` since it landed in stable as well. I made the commit messages as descriptive as possible so that it's easier to review relevant changes. ## Related issues - Supersedes #3602 - Fixes https://github.com/flutter/flutter/issues/119661 Question for reviewers: should the next version be 0.1.5 or 0.2.0 ? --- .../flutter_adaptive_scaffold/CHANGELOG.md | 3 +- packages/flutter_adaptive_scaffold/README.md | 198 +++++---- .../example/lib/adaptive_layout_demo.dart | 21 +- .../example/lib/main.dart | 394 ++++++++++-------- .../lib/src/adaptive_scaffold.dart | 307 +++++++------- .../flutter_adaptive_scaffold/pubspec.yaml | 2 +- .../test/adaptive_layout_test.dart | 16 +- .../test/adaptive_scaffold_test.dart | 37 ++ 8 files changed, 515 insertions(+), 463 deletions(-) diff --git a/packages/flutter_adaptive_scaffold/CHANGELOG.md b/packages/flutter_adaptive_scaffold/CHANGELOG.md index 0c907565f1ce..857f82fa02ae 100644 --- a/packages/flutter_adaptive_scaffold/CHANGELOG.md +++ b/packages/flutter_adaptive_scaffold/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 0.1.5 +* Added support for Right-to-left (RTL) directionality. * Fixes stale ignore: prefer_const_constructors. * Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. diff --git a/packages/flutter_adaptive_scaffold/README.md b/packages/flutter_adaptive_scaffold/README.md index 612e62c4bc92..e9324afbf1b1 100644 --- a/packages/flutter_adaptive_scaffold/README.md +++ b/packages/flutter_adaptive_scaffold/README.md @@ -137,110 +137,108 @@ displayed and the entrance animation and exit animation. ```dart - // AdaptiveLayout has a number of slots that take SlotLayouts and these - // SlotLayouts' configs take maps of Breakpoints to SlotLayoutConfigs. - return AdaptiveLayout( - // Primary navigation config has nothing from 0 to 600 dp screen width, - // then an unextended NavigationRail with no labels and just icons then an - // extended NavigationRail with both icons and labels. - primaryNavigation: SlotLayout( - config: { - Breakpoints.medium: SlotLayout.from( - inAnimation: AdaptiveScaffold.leftOutIn, - key: const Key('Primary Navigation Medium'), - builder: (_) => AdaptiveScaffold.standardNavigationRail( - selectedIndex: selectedNavigation, - onDestinationSelected: (int newIndex) { - setState(() { - selectedNavigation = newIndex; - }); - }, - leading: const Icon(Icons.menu), - destinations: destinations - .map((_) => AdaptiveScaffold.toRailDestination(_)) - .toList(), - backgroundColor: navRailTheme.backgroundColor, - selectedIconTheme: navRailTheme.selectedIconTheme, - unselectedIconTheme: navRailTheme.unselectedIconTheme, - selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, - unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle, - ), - ), - Breakpoints.large: SlotLayout.from( - key: const Key('Primary Navigation Large'), - inAnimation: AdaptiveScaffold.leftOutIn, - builder: (_) => AdaptiveScaffold.standardNavigationRail( - selectedIndex: selectedNavigation, - onDestinationSelected: (int newIndex) { - setState(() { - selectedNavigation = newIndex; - }); - }, - extended: true, - leading: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: const [ - Text( - 'REPLY', - style: TextStyle(color: Color.fromARGB(255, 255, 201, 197)), - ), - Icon(Icons.menu_open) - ], - ), - destinations: destinations - .map((_) => AdaptiveScaffold.toRailDestination(_)) - .toList(), - trailing: trailingNavRail, - backgroundColor: navRailTheme.backgroundColor, - selectedIconTheme: navRailTheme.selectedIconTheme, - unselectedIconTheme: navRailTheme.unselectedIconTheme, - selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, - unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle, - ), - ), - }, +// AdaptiveLayout has a number of slots that take SlotLayouts and these +// SlotLayouts' configs take maps of Breakpoints to SlotLayoutConfigs. +return AdaptiveLayout( + // Primary navigation config has nothing from 0 to 600 dp screen width, + // then an unextended NavigationRail with no labels and just icons then an + // extended NavigationRail with both icons and labels. + primaryNavigation: SlotLayout( + config: { + Breakpoints.medium: SlotLayout.from( + inAnimation: AdaptiveScaffold.leftOutIn, + key: const Key('Primary Navigation Medium'), + builder: (_) => AdaptiveScaffold.standardNavigationRail( + selectedIndex: selectedNavigation, + onDestinationSelected: (int newIndex) { + setState(() { + selectedNavigation = newIndex; + }); + }, + leading: const Icon(Icons.menu), + destinations: destinations + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + backgroundColor: navRailTheme.backgroundColor, + selectedIconTheme: navRailTheme.selectedIconTheme, + unselectedIconTheme: navRailTheme.unselectedIconTheme, + selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, + unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle, + ), ), - // Body switches between a ListView and a GridView from small to medium - // breakpoints and onwards. - body: SlotLayout( - config: { - Breakpoints.small: SlotLayout.from( - key: const Key('Body Small'), - builder: (_) => ListView.builder( - itemCount: children.length, - itemBuilder: (BuildContext context, int index) => children[index], - ), + Breakpoints.large: SlotLayout.from( + key: const Key('Primary Navigation Large'), + inAnimation: AdaptiveScaffold.leftOutIn, + builder: (_) => AdaptiveScaffold.standardNavigationRail( + selectedIndex: selectedNavigation, + onDestinationSelected: (int newIndex) { + setState(() { + selectedNavigation = newIndex; + }); + }, + extended: true, + leading: const Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Text( + 'REPLY', + style: TextStyle(color: Color.fromARGB(255, 255, 201, 197)), + ), + Icon(Icons.menu_open) + ], ), - Breakpoints.mediumAndUp: SlotLayout.from( - key: const Key('Body Medium'), - builder: (_) => - GridView.count(crossAxisCount: 2, children: children), - ) - }, + destinations: destinations + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + trailing: trailingNavRail, + backgroundColor: navRailTheme.backgroundColor, + selectedIconTheme: navRailTheme.selectedIconTheme, + unselectedIconTheme: navRailTheme.unselectedIconTheme, + selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, + unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle, + ), ), - // BottomNavigation is only active in small views defined as under 600 dp - // width. - bottomNavigation: SlotLayout( - config: { - Breakpoints.small: SlotLayout.from( - key: const Key('Bottom Navigation Small'), - inAnimation: AdaptiveScaffold.bottomToTop, - outAnimation: AdaptiveScaffold.topToBottom, - builder: (_) => AdaptiveScaffold.standardBottomNavigationBar( - destinations: destinations, - currentIndex: selectedNavigation, - onDestinationSelected: (int newIndex) { - setState(() { - selectedNavigation = newIndex; - }); - }, - ), - ) - }, + }, + ), + // Body switches between a ListView and a GridView from small to medium + // breakpoints and onwards. + body: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('Body Small'), + builder: (_) => ListView.builder( + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + ), ), - ); - } -} + Breakpoints.mediumAndUp: SlotLayout.from( + key: const Key('Body Medium'), + builder: (_) => + GridView.count(crossAxisCount: 2, children: children), + ) + }, + ), + // BottomNavigation is only active in small views defined as under 600 dp + // width. + bottomNavigation: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('Bottom Navigation Small'), + inAnimation: AdaptiveScaffold.bottomToTop, + outAnimation: AdaptiveScaffold.topToBottom, + builder: (_) => AdaptiveScaffold.standardBottomNavigationBar( + destinations: destinations, + currentIndex: selectedNavigation, + onDestinationSelected: (int newIndex) { + setState(() { + selectedNavigation = newIndex; + }); + }, + ), + ) + }, + ), +); ``` Both of the examples shown here produce the same output: diff --git a/packages/flutter_adaptive_scaffold/example/lib/adaptive_layout_demo.dart b/packages/flutter_adaptive_scaffold/example/lib/adaptive_layout_demo.dart index eb9522041fce..77b5cd78d9fe 100644 --- a/packages/flutter_adaptive_scaffold/example/lib/adaptive_layout_demo.dart +++ b/packages/flutter_adaptive_scaffold/example/lib/adaptive_layout_demo.dart @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// TODO(goderbauer): Remove this ignore when this package requires Flutter 3.8 or later. -// ignore_for_file: prefer_const_constructors - import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; @@ -58,8 +55,8 @@ class _MyHomePageState extends State { children: [ const Divider(color: Colors.black), const SizedBox(height: 10), - Row( - children: const [ + const Row( + children: [ SizedBox(width: 27), Text('Folders', style: TextStyle(fontSize: 16)), ], @@ -74,8 +71,8 @@ class _MyHomePageState extends State { iconSize: 21, ), const SizedBox(width: 21), - Flexible( - child: const Text( + const Flexible( + child: Text( 'Freelance', overflow: TextOverflow.ellipsis, ), @@ -92,8 +89,8 @@ class _MyHomePageState extends State { iconSize: 21, ), const SizedBox(width: 21), - Flexible( - child: const Text( + const Flexible( + child: Text( 'Mortgage', overflow: TextOverflow.ellipsis, ), @@ -198,9 +195,9 @@ class _MyHomePageState extends State { }); }, extended: true, - leading: Row( + leading: const Row( mainAxisAlignment: MainAxisAlignment.spaceAround, - children: const [ + children: [ Text( 'REPLY', style: TextStyle(color: Color.fromARGB(255, 255, 201, 197)), @@ -260,6 +257,6 @@ class _MyHomePageState extends State { }, ), ); - // #enddocregion + // #enddocregion Example } } diff --git a/packages/flutter_adaptive_scaffold/example/lib/main.dart b/packages/flutter_adaptive_scaffold/example/lib/main.dart index 5e97f06859a9..4aac90d8bc93 100644 --- a/packages/flutter_adaptive_scaffold/example/lib/main.dart +++ b/packages/flutter_adaptive_scaffold/example/lib/main.dart @@ -52,6 +52,9 @@ class _MyHomePageState extends State // the navigation elements. ValueNotifier showGridView = ValueNotifier(false); + // Override the application's directionality. + TextDirection directionalityOverride = TextDirection.ltr; + // The index of the selected mail card. int? selected; @@ -118,70 +121,96 @@ class _MyHomePageState extends State @override Widget build(BuildContext context) { - final Widget trailingNavRail = Column( - children: [ - const Divider(color: Colors.white, thickness: 1.5), - const SizedBox(height: 10), - Row(children: [ - const SizedBox(width: 22), - Text('Folders', - style: TextStyle(fontSize: 13, color: Colors.grey[700])) - ]), - const SizedBox(height: 22), - Row( - children: [ - const SizedBox(width: 16), - IconButton( - onPressed: () {}, - icon: const Icon(Icons.folder_copy_outlined), - iconSize: 21, - ), - const SizedBox(width: 21), - const Text('Freelance', overflow: TextOverflow.ellipsis), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - const SizedBox(width: 16), - IconButton( - onPressed: () {}, - icon: const Icon(Icons.folder_copy_outlined), - iconSize: 21, - ), - const SizedBox(width: 21), - const Text('Mortgage', overflow: TextOverflow.ellipsis), - ], - ), - const SizedBox(height: 16), - Row( - children: [ - const SizedBox(width: 16), - IconButton( - onPressed: () {}, - icon: const Icon(Icons.folder_copy_outlined), - iconSize: 21, - ), - const SizedBox(width: 21), - const Flexible( - child: Text('Taxes', overflow: TextOverflow.ellipsis)) - ], - ), - const SizedBox(height: 16), - Row( - children: [ - const SizedBox(width: 16), - IconButton( - onPressed: () {}, - icon: const Icon(Icons.folder_copy_outlined), - iconSize: 21, + final Widget trailingNavRail = Expanded( + child: Column( + children: [ + const Divider(color: Colors.white, thickness: 1.5), + const SizedBox(height: 10), + Row(children: [ + const SizedBox(width: 22), + Text('Folders', + style: TextStyle(fontSize: 13, color: Colors.grey[700])) + ]), + const SizedBox(height: 22), + Row( + children: [ + const SizedBox(width: 16), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.folder_copy_outlined), + iconSize: 21, + ), + const SizedBox(width: 21), + const Text('Freelance', overflow: TextOverflow.ellipsis), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + const SizedBox(width: 16), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.folder_copy_outlined), + iconSize: 21, + ), + const SizedBox(width: 21), + const Text('Mortgage', overflow: TextOverflow.ellipsis), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + const SizedBox(width: 16), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.folder_copy_outlined), + iconSize: 21, + ), + const SizedBox(width: 21), + const Flexible( + child: Text('Taxes', overflow: TextOverflow.ellipsis)) + ], + ), + const SizedBox(height: 16), + Row( + children: [ + const SizedBox(width: 16), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.folder_copy_outlined), + iconSize: 21, + ), + const SizedBox(width: 21), + const Flexible( + child: Text('Receipts', overflow: TextOverflow.ellipsis)) + ], + ), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: SwitchListTile.adaptive( + title: const Text( + 'Directionality', + style: TextStyle(fontSize: 12), + ), + subtitle: Text( + directionalityOverride == TextDirection.ltr ? 'LTR' : 'RTL', + ), + value: directionalityOverride == TextDirection.ltr, + onChanged: (bool value) { + setState(() { + if (value) { + directionalityOverride = TextDirection.ltr; + } else { + directionalityOverride = TextDirection.rtl; + } + }); + }, + ), ), - const SizedBox(width: 21), - const Flexible( - child: Text('Receipts', overflow: TextOverflow.ellipsis)) - ], - ), - ], + ), + ], + ), ); // These are the destinations used within the AdaptiveScaffold navigation @@ -208,134 +237,137 @@ class _MyHomePageState extends State // Updating the listener value. showGridView.value = Breakpoints.mediumAndUp.isActive(context); - return Scaffold( - backgroundColor: const Color.fromARGB(255, 234, 227, 241), - // Usage of AdaptiveLayout suite begins here. AdaptiveLayout takes - // LayoutSlots for its variety of screen slots. - body: AdaptiveLayout( - // Each SlotLayout has a config which maps Breakpoints to - // SlotLayoutConfigs. - primaryNavigation: SlotLayout( - config: { - // The breakpoint used here is from the Breakpoints class but custom - // Breakpoints can be defined by extending the Breakpoint class - Breakpoints.medium: SlotLayout.from( - // Every SlotLayoutConfig takes a key and a builder. The builder - // is to save memory that would be spent on initialization. - key: const Key('primaryNavigation'), - builder: (_) { - return AdaptiveScaffold.standardNavigationRail( - // Usually it would be easier to use a builder from - // AdaptiveScaffold for these types of navigation but this - // navigation has custom staggered item animations. + return Directionality( + textDirection: directionalityOverride, + child: Scaffold( + backgroundColor: const Color.fromARGB(255, 234, 227, 241), + // Usage of AdaptiveLayout suite begins here. AdaptiveLayout takes + // LayoutSlots for its variety of screen slots. + body: AdaptiveLayout( + // Each SlotLayout has a config which maps Breakpoints to + // SlotLayoutConfigs. + primaryNavigation: SlotLayout( + config: { + // The breakpoint used here is from the Breakpoints class but custom + // Breakpoints can be defined by extending the Breakpoint class + Breakpoints.medium: SlotLayout.from( + // Every SlotLayoutConfig takes a key and a builder. The builder + // is to save memory that would be spent on initialization. + key: const Key('primaryNavigation'), + builder: (_) { + return AdaptiveScaffold.standardNavigationRail( + // Usually it would be easier to use a builder from + // AdaptiveScaffold for these types of navigation but this + // navigation has custom staggered item animations. + onDestinationSelected: (int index) { + setState(() { + _navigationIndex = index; + }); + }, + selectedIndex: _navigationIndex, + leading: ScaleTransition( + scale: _articleIconSlideController, + child: const _MediumComposeIcon(), + ), + backgroundColor: const Color.fromARGB(0, 255, 255, 255), + destinations: [ + slideInNavigationItem( + begin: -1, + controller: _inboxIconSlideController, + icon: Icons.inbox, + label: 'Inbox', + ), + slideInNavigationItem( + begin: -2, + controller: _articleIconSlideController, + icon: Icons.article_outlined, + label: 'Articles', + ), + slideInNavigationItem( + begin: -3, + controller: _chatIconSlideController, + icon: Icons.chat_bubble_outline, + label: 'Chat', + ), + slideInNavigationItem( + begin: -4, + controller: _videoIconSlideController, + icon: Icons.video_call_outlined, + label: 'Video', + ) + ], + ); + }, + ), + Breakpoints.large: SlotLayout.from( + key: const Key('Large primaryNavigation'), + // The AdaptiveScaffold builder here greatly simplifies + // navigational elements. + builder: (_) => AdaptiveScaffold.standardNavigationRail( + leading: const _LargeComposeIcon(), onDestinationSelected: (int index) { setState(() { _navigationIndex = index; }); }, selectedIndex: _navigationIndex, - leading: ScaleTransition( - scale: _articleIconSlideController, - child: const _MediumComposeIcon(), - ), - backgroundColor: const Color.fromARGB(0, 255, 255, 255), - destinations: [ - slideInNavigationItem( - begin: -1, - controller: _inboxIconSlideController, - icon: Icons.inbox, - label: 'Inbox', - ), - slideInNavigationItem( - begin: -2, - controller: _articleIconSlideController, - icon: Icons.article_outlined, - label: 'Articles', - ), - slideInNavigationItem( - begin: -3, - controller: _chatIconSlideController, - icon: Icons.chat_bubble_outline, - label: 'Chat', - ), - slideInNavigationItem( - begin: -4, - controller: _videoIconSlideController, - icon: Icons.video_call_outlined, - label: 'Video', - ) - ], - ); - }, - ), - Breakpoints.large: SlotLayout.from( - key: const Key('Large primaryNavigation'), - // The AdaptiveScaffold builder here greatly simplifies - // navigational elements. - builder: (_) => AdaptiveScaffold.standardNavigationRail( - leading: const _LargeComposeIcon(), - onDestinationSelected: (int index) { - setState(() { - _navigationIndex = index; - }); - }, - selectedIndex: _navigationIndex, - trailing: trailingNavRail, - extended: true, - destinations: destinations.map((_) { - return AdaptiveScaffold.toRailDestination(_); - }).toList(), + trailing: trailingNavRail, + extended: true, + destinations: destinations.map((_) { + return AdaptiveScaffold.toRailDestination(_); + }).toList(), + ), ), - ), - }, - ), - body: SlotLayout( - config: { - Breakpoints.standard: SlotLayout.from( - key: const Key('body'), - // The conditional here is for navigation screens. The first - // screen shows the main screen and every other screen shows - // ExamplePage. - builder: (_) => (_navigationIndex == 0) - ? Padding( - padding: const EdgeInsets.fromLTRB(0, 32, 0, 0), - child: _ItemList( - selected: selected, - items: _allItems, - selectCard: selectCard, + }, + ), + body: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('body'), + // The conditional here is for navigation screens. The first + // screen shows the main screen and every other screen shows + // ExamplePage. + builder: (_) => (_navigationIndex == 0) + ? Padding( + padding: const EdgeInsets.fromLTRB(0, 32, 0, 0), + child: _ItemList( + selected: selected, + items: _allItems, + selectCard: selectCard, + ), + ) + : const _ExamplePage(), + ), + }, + ), + secondaryBody: _navigationIndex == 0 + ? SlotLayout( + config: { + Breakpoints.mediumAndUp: SlotLayout.from( + // This overrides the default behavior of the secondaryBody + // disappearing as it is animating out. + outAnimation: AdaptiveScaffold.stayOnScreen, + key: const Key('Secondary Body'), + builder: (_) => SafeArea( + child: _DetailTile(item: _allItems[selected ?? 0]), ), ) - : const _ExamplePage(), - ), - }, - ), - secondaryBody: _navigationIndex == 0 - ? SlotLayout( - config: { - Breakpoints.mediumAndUp: SlotLayout.from( - // This overrides the default behavior of the secondaryBody - // disappearing as it is animating out. - outAnimation: AdaptiveScaffold.stayOnScreen, - key: const Key('Secondary Body'), - builder: (_) => SafeArea( - child: _DetailTile(item: _allItems[selected ?? 0]), - ), - ) - }, + }, + ) + : null, + bottomNavigation: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('bottomNavigation'), + // You can define inAnimations or outAnimations to override the + // default offset transition. + outAnimation: AdaptiveScaffold.topToBottom, + builder: (_) => AdaptiveScaffold.standardBottomNavigationBar( + destinations: destinations, + ), ) - : null, - bottomNavigation: SlotLayout( - config: { - Breakpoints.small: SlotLayout.from( - key: const Key('bottomNavigation'), - // You can define inAnimations or outAnimations to override the - // default offset transition. - outAnimation: AdaptiveScaffold.topToBottom, - builder: (_) => AdaptiveScaffold.standardBottomNavigationBar( - destinations: destinations, - ), - ) - }, + }, + ), ), ), ); diff --git a/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart b/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart index 028f47e7cad7..f3485ae370bf 100644 --- a/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart +++ b/packages/flutter_adaptive_scaffold/lib/src/adaptive_scaffold.dart @@ -488,165 +488,160 @@ class _AdaptiveScaffoldState extends State { final NavigationRailThemeData navRailTheme = Theme.of(context).navigationRailTheme; - return Directionality( - textDirection: TextDirection.ltr, - child: Scaffold( - appBar: widget.drawerBreakpoint.isActive(context) && widget.useDrawer - ? widget.appBar ?? AppBar() - : null, - drawer: widget.drawerBreakpoint.isActive(context) && widget.useDrawer - ? Drawer( - child: NavigationRail( - extended: true, - leading: widget.leadingExtendedNavRail, - trailing: widget.trailingNavRail, - selectedIndex: widget.selectedIndex, - destinations: widget.destinations - .map((_) => AdaptiveScaffold.toRailDestination(_)) - .toList(), - onDestinationSelected: widget.onSelectedIndexChange, - ), - ) - : null, - body: AdaptiveLayout( - bodyOrientation: widget.bodyOrientation, - bodyRatio: widget.bodyRatio, - internalAnimations: widget.internalAnimations, - primaryNavigation: SlotLayout( - config: { - widget.mediumBreakpoint: SlotLayout.from( - key: const Key('primaryNavigation'), - builder: (_) => AdaptiveScaffold.standardNavigationRail( - width: widget.navigationRailWidth, - leading: widget.leadingUnextendedNavRail, - trailing: widget.trailingNavRail, - selectedIndex: widget.selectedIndex, - destinations: widget.destinations - .map((_) => AdaptiveScaffold.toRailDestination(_)) - .toList(), - onDestinationSelected: widget.onSelectedIndexChange, - backgroundColor: navRailTheme.backgroundColor, - selectedIconTheme: navRailTheme.selectedIconTheme, - unselectedIconTheme: navRailTheme.unselectedIconTheme, - selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, - unSelectedLabelTextStyle: - navRailTheme.unselectedLabelTextStyle, - ), + return Scaffold( + appBar: widget.drawerBreakpoint.isActive(context) && widget.useDrawer + ? widget.appBar ?? AppBar() + : null, + drawer: widget.drawerBreakpoint.isActive(context) && widget.useDrawer + ? Drawer( + child: NavigationRail( + extended: true, + leading: widget.leadingExtendedNavRail, + trailing: widget.trailingNavRail, + selectedIndex: widget.selectedIndex, + destinations: widget.destinations + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + onDestinationSelected: widget.onSelectedIndexChange, ), - widget.largeBreakpoint: SlotLayout.from( - key: const Key('primaryNavigation1'), - builder: (_) => AdaptiveScaffold.standardNavigationRail( - width: widget.extendedNavigationRailWidth, - extended: true, - leading: widget.leadingExtendedNavRail, - trailing: widget.trailingNavRail, - selectedIndex: widget.selectedIndex, - destinations: widget.destinations - .map((_) => AdaptiveScaffold.toRailDestination(_)) - .toList(), - onDestinationSelected: widget.onSelectedIndexChange, - backgroundColor: navRailTheme.backgroundColor, - selectedIconTheme: navRailTheme.selectedIconTheme, - unselectedIconTheme: navRailTheme.unselectedIconTheme, - selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, - unSelectedLabelTextStyle: - navRailTheme.unselectedLabelTextStyle, - ), + ) + : null, + body: AdaptiveLayout( + bodyOrientation: widget.bodyOrientation, + bodyRatio: widget.bodyRatio, + internalAnimations: widget.internalAnimations, + primaryNavigation: SlotLayout( + config: { + widget.mediumBreakpoint: SlotLayout.from( + key: const Key('primaryNavigation'), + builder: (_) => AdaptiveScaffold.standardNavigationRail( + width: widget.navigationRailWidth, + leading: widget.leadingUnextendedNavRail, + trailing: widget.trailingNavRail, + selectedIndex: widget.selectedIndex, + destinations: widget.destinations + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + onDestinationSelected: widget.onSelectedIndexChange, + backgroundColor: navRailTheme.backgroundColor, + selectedIconTheme: navRailTheme.selectedIconTheme, + unselectedIconTheme: navRailTheme.unselectedIconTheme, + selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, + unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle, ), - }, - ), - bottomNavigation: - !widget.drawerBreakpoint.isActive(context) || !widget.useDrawer - ? SlotLayout( - config: { - widget.smallBreakpoint: SlotLayout.from( - key: const Key('bottomNavigation'), - builder: (_) => - AdaptiveScaffold.standardBottomNavigationBar( - currentIndex: widget.selectedIndex, - destinations: widget.destinations, - onDestinationSelected: widget.onSelectedIndexChange, - ), - ), - }, - ) - : null, - body: SlotLayout( - config: { - Breakpoints.standard: SlotLayout.from( - key: const Key('body'), - inAnimation: AdaptiveScaffold.fadeIn, - outAnimation: AdaptiveScaffold.fadeOut, - builder: widget.body, - ), - if (widget.smallBody != null) - widget.smallBreakpoint: - (widget.smallBody != AdaptiveScaffold.emptyBuilder) - ? SlotLayout.from( - key: const Key('smallBody'), - inAnimation: AdaptiveScaffold.fadeIn, - outAnimation: AdaptiveScaffold.fadeOut, - builder: widget.smallBody, - ) - : null, - if (widget.body != null) - widget.mediumBreakpoint: - (widget.body != AdaptiveScaffold.emptyBuilder) - ? SlotLayout.from( - key: const Key('body'), - inAnimation: AdaptiveScaffold.fadeIn, - outAnimation: AdaptiveScaffold.fadeOut, - builder: widget.body, - ) - : null, - if (widget.largeBody != null) - widget.largeBreakpoint: - (widget.largeBody != AdaptiveScaffold.emptyBuilder) - ? SlotLayout.from( - key: const Key('largeBody'), - inAnimation: AdaptiveScaffold.fadeIn, - outAnimation: AdaptiveScaffold.fadeOut, - builder: widget.largeBody, - ) - : null, - }, - ), - secondaryBody: SlotLayout( - config: { - Breakpoints.standard: SlotLayout.from( - key: const Key('sBody'), - outAnimation: AdaptiveScaffold.stayOnScreen, - builder: widget.secondaryBody, + ), + widget.largeBreakpoint: SlotLayout.from( + key: const Key('primaryNavigation1'), + builder: (_) => AdaptiveScaffold.standardNavigationRail( + width: widget.extendedNavigationRailWidth, + extended: true, + leading: widget.leadingExtendedNavRail, + trailing: widget.trailingNavRail, + selectedIndex: widget.selectedIndex, + destinations: widget.destinations + .map((_) => AdaptiveScaffold.toRailDestination(_)) + .toList(), + onDestinationSelected: widget.onSelectedIndexChange, + backgroundColor: navRailTheme.backgroundColor, + selectedIconTheme: navRailTheme.selectedIconTheme, + unselectedIconTheme: navRailTheme.unselectedIconTheme, + selectedLabelTextStyle: navRailTheme.selectedLabelTextStyle, + unSelectedLabelTextStyle: navRailTheme.unselectedLabelTextStyle, ), - if (widget.smallSecondaryBody != null) - widget.smallBreakpoint: - (widget.smallSecondaryBody != AdaptiveScaffold.emptyBuilder) - ? SlotLayout.from( - key: const Key('smallSBody'), - outAnimation: AdaptiveScaffold.stayOnScreen, - builder: widget.smallSecondaryBody, - ) - : null, - if (widget.secondaryBody != null) - widget.mediumBreakpoint: - (widget.secondaryBody != AdaptiveScaffold.emptyBuilder) - ? SlotLayout.from( - key: const Key('sBody'), - outAnimation: AdaptiveScaffold.stayOnScreen, - builder: widget.secondaryBody, - ) - : null, - if (widget.largeSecondaryBody != null) - widget.largeBreakpoint: - (widget.largeSecondaryBody != AdaptiveScaffold.emptyBuilder) - ? SlotLayout.from( - key: const Key('largeSBody'), - outAnimation: AdaptiveScaffold.stayOnScreen, - builder: widget.largeSecondaryBody, - ) - : null, - }, - ), + ), + }, + ), + bottomNavigation: + !widget.drawerBreakpoint.isActive(context) || !widget.useDrawer + ? SlotLayout( + config: { + widget.smallBreakpoint: SlotLayout.from( + key: const Key('bottomNavigation'), + builder: (_) => + AdaptiveScaffold.standardBottomNavigationBar( + currentIndex: widget.selectedIndex, + destinations: widget.destinations, + onDestinationSelected: widget.onSelectedIndexChange, + ), + ), + }, + ) + : null, + body: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('body'), + inAnimation: AdaptiveScaffold.fadeIn, + outAnimation: AdaptiveScaffold.fadeOut, + builder: widget.body, + ), + if (widget.smallBody != null) + widget.smallBreakpoint: + (widget.smallBody != AdaptiveScaffold.emptyBuilder) + ? SlotLayout.from( + key: const Key('smallBody'), + inAnimation: AdaptiveScaffold.fadeIn, + outAnimation: AdaptiveScaffold.fadeOut, + builder: widget.smallBody, + ) + : null, + if (widget.body != null) + widget.mediumBreakpoint: + (widget.body != AdaptiveScaffold.emptyBuilder) + ? SlotLayout.from( + key: const Key('body'), + inAnimation: AdaptiveScaffold.fadeIn, + outAnimation: AdaptiveScaffold.fadeOut, + builder: widget.body, + ) + : null, + if (widget.largeBody != null) + widget.largeBreakpoint: + (widget.largeBody != AdaptiveScaffold.emptyBuilder) + ? SlotLayout.from( + key: const Key('largeBody'), + inAnimation: AdaptiveScaffold.fadeIn, + outAnimation: AdaptiveScaffold.fadeOut, + builder: widget.largeBody, + ) + : null, + }, + ), + secondaryBody: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('sBody'), + outAnimation: AdaptiveScaffold.stayOnScreen, + builder: widget.secondaryBody, + ), + if (widget.smallSecondaryBody != null) + widget.smallBreakpoint: + (widget.smallSecondaryBody != AdaptiveScaffold.emptyBuilder) + ? SlotLayout.from( + key: const Key('smallSBody'), + outAnimation: AdaptiveScaffold.stayOnScreen, + builder: widget.smallSecondaryBody, + ) + : null, + if (widget.secondaryBody != null) + widget.mediumBreakpoint: + (widget.secondaryBody != AdaptiveScaffold.emptyBuilder) + ? SlotLayout.from( + key: const Key('sBody'), + outAnimation: AdaptiveScaffold.stayOnScreen, + builder: widget.secondaryBody, + ) + : null, + if (widget.largeSecondaryBody != null) + widget.largeBreakpoint: + (widget.largeSecondaryBody != AdaptiveScaffold.emptyBuilder) + ? SlotLayout.from( + key: const Key('largeSBody'), + outAnimation: AdaptiveScaffold.stayOnScreen, + builder: widget.largeSecondaryBody, + ) + : null, + }, ), ), ); diff --git a/packages/flutter_adaptive_scaffold/pubspec.yaml b/packages/flutter_adaptive_scaffold/pubspec.yaml index 4e3cc5c57d42..155e08665b41 100644 --- a/packages/flutter_adaptive_scaffold/pubspec.yaml +++ b/packages/flutter_adaptive_scaffold/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_adaptive_scaffold description: Widgets to easily build adaptive layouts, including navigation elements. -version: 0.1.4 +version: 0.1.5 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+flutter_adaptive_scaffold%22 repository: https://github.com/flutter/packages/tree/main/packages/flutter_adaptive_scaffold diff --git a/packages/flutter_adaptive_scaffold/test/adaptive_layout_test.dart b/packages/flutter_adaptive_scaffold/test/adaptive_layout_test.dart index 90fb25bffdb0..5a84f4476e41 100644 --- a/packages/flutter_adaptive_scaffold/test/adaptive_layout_test.dart +++ b/packages/flutter_adaptive_scaffold/test/adaptive_layout_test.dart @@ -170,9 +170,7 @@ void main() { expect(begin, findsOneWidget); expect(end, findsOneWidget); } - // TODO(gspencergoog): Remove skip when AnimatedSwitcher fix rolls into stable. - // https://github.com/flutter/flutter/pull/107476 - }, skip: true); + }); testWidgets('slot layout can tolerate rapid changes in breakpoints', (WidgetTester tester) async { @@ -191,9 +189,7 @@ void main() { await tester.pumpAndSettle(); expect(begin, findsOneWidget); expect(end, findsNothing); - // TODO(a-wallen): Remove skip when AnimatedSwitcher fix rolls into stable. - // https://github.com/flutter/flutter/pull/107476 - }, skip: true); + }); // This test reflects the behavior of the internal animations of both the body // and secondary body and also the navigational items. This is reflected in @@ -248,9 +244,7 @@ void main() { expect(tester.getTopLeft(secondaryTestBreakpoint), const Offset(200, 10)); expect( tester.getBottomRight(secondaryTestBreakpoint), const Offset(390, 790)); - // TODO(a-wallen): Remove skip when AnimatedSwitcher fix rolls into stable. - // https://github.com/flutter/flutter/pull/107476 - }, skip: true); + }); testWidgets('adaptive layout does not animate when animations off', (WidgetTester tester) async { @@ -269,9 +263,7 @@ void main() { expect(tester.getTopLeft(secondaryTestBreakpoint), const Offset(200, 10)); expect( tester.getBottomRight(secondaryTestBreakpoint), const Offset(390, 790)); - // TODO(a-wallen): Remove skip when AnimatedSwitcher fix rolls into stable. - // https://github.com/flutter/flutter/pull/107476 - }, skip: true); + }); } class TestBreakpoint0 extends Breakpoint { diff --git a/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart b/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart index 325628645522..463dd84a0b3b 100644 --- a/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart +++ b/packages/flutter_adaptive_scaffold/test/adaptive_scaffold_test.dart @@ -543,6 +543,43 @@ void main() { tester.widget(find.byType(NavigationRail)); expect(rail.groupAlignment, equals(groupAlignment)); }); + + testWidgets( + "doesn't override Directionality", + (WidgetTester tester) async { + const List destinations = [ + NavigationDestination( + icon: Icon(Icons.home), + label: 'Home', + ), + NavigationDestination( + icon: Icon(Icons.account_circle), + label: 'Profile', + ), + ]; + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Directionality( + textDirection: TextDirection.rtl, + child: AdaptiveScaffold( + destinations: destinations, + body: (BuildContext context) { + return const SizedBox.shrink(); + }, + ), + ), + ), + ), + ); + + final Finder body = find.byKey(const Key('body')); + expect(body, findsOneWidget); + final TextDirection dir = Directionality.of(body.evaluate().first); + expect(dir, TextDirection.rtl); + }, + ); } /// An empty widget that implements [PreferredSizeWidget] to ensure that From b589429d3a7157931e285da732b27d04e5021e89 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 14 Jun 2023 17:54:13 -0400 Subject: [PATCH 30/53] [pigeon] Enable Obj-C integration tests in CI (#4215) Enables the Obj-C integration tests now that CI has been updated. --- packages/pigeon/tool/run_tests.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/pigeon/tool/run_tests.dart b/packages/pigeon/tool/run_tests.dart index fd00ecf0fac1..6cc96f2b2069 100644 --- a/packages/pigeon/tool/run_tests.dart +++ b/packages/pigeon/tool/run_tests.dart @@ -121,9 +121,7 @@ Future main(List args) async { ]; const List macOSHostTests = [ iOSObjCUnitTests, - // TODO(stuartmorgan): Enable by default once CI issues are solved; see - // https://github.com/flutter/packages/pull/2816. - //iOSObjCIntegrationTests, + iOSObjCIntegrationTests, // Currently these are testing exactly the same thing as // macOSSwiftIntegrationTests, so we don't need to run both by default. This // should be enabled if any iOS-only tests are added (e.g., for a feature From cef91bc73004e3bdfbd131489fd2d53027777cec Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Wed, 14 Jun 2023 15:33:07 -0700 Subject: [PATCH 31/53] Ignore `textScaleFactor` deprecation (#4209) Ignore `textScaleFactor` deprecation, as suggested in https://github.com/flutter/flutter/issues/128825 --- .../flutter_markdown/lib/src/_functions_io.dart | 3 ++- .../flutter_markdown/lib/src/_functions_web.dart | 3 ++- packages/flutter_markdown/lib/src/builder.dart | 6 ++++-- .../test/text_scale_factor_test.dart | 13 ++++++++----- packages/rfw/lib/src/flutter/core_widgets.dart | 2 +- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/flutter_markdown/lib/src/_functions_io.dart b/packages/flutter_markdown/lib/src/_functions_io.dart index 7c35ba485333..b3fa99ea63e6 100644 --- a/packages/flutter_markdown/lib/src/_functions_io.dart +++ b/packages/flutter_markdown/lib/src/_functions_io.dart @@ -66,7 +66,8 @@ final MarkdownStyleSheet Function(BuildContext, MarkdownStyleSheetBaseTheme?) } return result.copyWith( - textScaleFactor: MediaQuery.textScaleFactorOf(context), + textScaleFactor: + MediaQuery.textScaleFactorOf(context), // ignore: deprecated_member_use ); }; diff --git a/packages/flutter_markdown/lib/src/_functions_web.dart b/packages/flutter_markdown/lib/src/_functions_web.dart index a58a9cea37d6..828388613a53 100644 --- a/packages/flutter_markdown/lib/src/_functions_web.dart +++ b/packages/flutter_markdown/lib/src/_functions_web.dart @@ -68,7 +68,8 @@ final MarkdownStyleSheet Function(BuildContext, MarkdownStyleSheetBaseTheme?) } return result.copyWith( - textScaleFactor: MediaQuery.textScaleFactorOf(context), + textScaleFactor: + MediaQuery.textScaleFactorOf(context), // ignore: deprecated_member_use ); }; diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart index 475c510be98f..b688427e8cdd 100644 --- a/packages/flutter_markdown/lib/src/builder.dart +++ b/packages/flutter_markdown/lib/src/builder.dart @@ -829,7 +829,8 @@ class MarkdownBuilder implements md.NodeVisitor { if (selectable) { return SelectableText.rich( text!, - textScaleFactor: styleSheet.textScaleFactor, + textScaleFactor: + styleSheet.textScaleFactor, // ignore: deprecated_member_use textAlign: textAlign ?? TextAlign.start, onTap: onTapText, key: k, @@ -837,7 +838,8 @@ class MarkdownBuilder implements md.NodeVisitor { } else { return RichText( text: text!, - textScaleFactor: styleSheet.textScaleFactor!, + textScaleFactor: + styleSheet.textScaleFactor!, // ignore: deprecated_member_use textAlign: textAlign ?? TextAlign.start, key: k, ); diff --git a/packages/flutter_markdown/test/text_scale_factor_test.dart b/packages/flutter_markdown/test/text_scale_factor_test.dart index 3710b3e0a62e..41b5ec7c9beb 100644 --- a/packages/flutter_markdown/test/text_scale_factor_test.dart +++ b/packages/flutter_markdown/test/text_scale_factor_test.dart @@ -25,7 +25,7 @@ void defineTests() { ); final RichText richText = tester.widget(find.byType(RichText)); - expect(richText.textScaleFactor, 2.0); + expect(richText.textScaleFactor, 2.0); // ignore: deprecated_member_use }, ); @@ -36,7 +36,8 @@ void defineTests() { await tester.pumpWidget( boilerplate( const MediaQuery( - data: MediaQueryData(textScaleFactor: 2.0), + data: MediaQueryData( + textScaleFactor: 2.0), // ignore: deprecated_member_use child: MarkdownBody( data: data, ), @@ -45,7 +46,7 @@ void defineTests() { ); final RichText richText = tester.widget(find.byType(RichText)); - expect(richText.textScaleFactor, 2.0); + expect(richText.textScaleFactor, 2.0); // ignore: deprecated_member_use }, ); @@ -56,7 +57,8 @@ void defineTests() { await tester.pumpWidget( boilerplate( const MediaQuery( - data: MediaQueryData(textScaleFactor: 2.0), + data: MediaQueryData( + textScaleFactor: 2.0), // ignore: deprecated_member_use child: MarkdownBody( data: data, selectable: true, @@ -67,7 +69,8 @@ void defineTests() { final SelectableText selectableText = tester.widget(find.byType(SelectableText)); - expect(selectableText.textScaleFactor, 2.0); + expect(selectableText.textScaleFactor, + 2.0); // ignore: deprecated_member_use }, ); }); diff --git a/packages/rfw/lib/src/flutter/core_widgets.dart b/packages/rfw/lib/src/flutter/core_widgets.dart index 0ff1b1226979..99a9f7a04c5b 100644 --- a/packages/rfw/lib/src/flutter/core_widgets.dart +++ b/packages/rfw/lib/src/flutter/core_widgets.dart @@ -644,7 +644,7 @@ Map get _coreWidgetsDefinitions => (['softWrap']), overflow: ArgumentDecoders.enumValue(TextOverflow.values, source, ['overflow']), - textScaleFactor: source.v(['textScaleFactor']), + textScaleFactor: source.v(['textScaleFactor']), // ignore: deprecated_member_use maxLines: source.v(['maxLines']), semanticsLabel: source.v(['semanticsLabel']), textWidthBasis: ArgumentDecoders.enumValue(TextWidthBasis.values, source, ['textWidthBasis']), From 786989601864517c56f48fbcd612863c56e1cba9 Mon Sep 17 00:00:00 2001 From: Tarrin Neal Date: Wed, 14 Jun 2023 17:34:05 -0700 Subject: [PATCH 32/53] [image_picker] getMedia platform implementations (#4175) Adds `getMedia` and `getMultipleMedia` methods to all image_picker platforms. ~~waiting on https://github.com/flutter/packages/pull/4174~~ precursor to https://github.com/flutter/packages/pull/3892 part of https://github.com/flutter/flutter/issues/89159 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [relevant style guides] and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages repo does use `dart format`.) - [x] I signed the [CLA]. - [x] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[shared_preferences]` - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or this PR is [exempt from version changes]. - [x] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style]. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. [Contributor Guide]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [relevant style guides]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md#style [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat [pub versioning philosophy]: https://dart.dev/tools/pub/versioning [exempt from version changes]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#version-and-changelog-updates [following repository CHANGELOG style]: https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#changelog-style [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests --- .../image_picker_android/CHANGELOG.md | 4 + .../imagepicker/ImagePickerDelegate.java | 163 +++++++++--- .../imagepicker/ImagePickerPlugin.java | 45 +++- .../flutter/plugins/imagepicker/Messages.java | 204 +++++++++++++-- .../imagepicker/ImagePickerDelegateTest.java | 29 +++ .../imagepicker/ImagePickerPluginTest.java | 106 +++++++- .../example/lib/main.dart | 164 +++++++++--- .../image_picker_android/example/pubspec.yaml | 3 +- .../lib/image_picker_android.dart | 104 ++++++-- .../lib/src/messages.g.dart | 126 ++++++++-- .../pigeons/messages.dart | 37 ++- .../image_picker_android/pubspec.yaml | 4 +- .../test/image_picker_android_test.dart | 159 +++++++++++- .../image_picker_android/test/test_api.g.dart | 87 +++++-- .../image_picker_for_web/CHANGELOG.md | 3 +- .../image_picker_for_web_test.dart | 35 ++- .../image_picker_for_web/example/pubspec.yaml | 2 +- .../lib/image_picker_for_web.dart | 27 +- .../image_picker_for_web/pubspec.yaml | 5 +- .../image_picker_ios/CHANGELOG.md | 5 + .../ios/RunnerTests/ImagePickerPluginTests.m | 78 ++++++ .../image_picker_ios/example/lib/main.dart | 168 ++++++++++--- .../image_picker_ios/example/pubspec.yaml | 3 +- .../Classes/FLTImagePickerPhotoAssetUtil.h | 5 +- .../Classes/FLTImagePickerPhotoAssetUtil.m | 14 ++ .../ios/Classes/FLTImagePickerPlugin.m | 65 +++-- .../ios/Classes/FLTImagePickerPlugin_Test.h | 5 +- .../FLTPHPickerSaveImageToPathOperation.m | 45 +++- .../image_picker_ios/ios/Classes/messages.g.h | 20 +- .../image_picker_ios/ios/Classes/messages.g.m | 75 +++++- .../lib/image_picker_ios.dart | 45 ++++ .../image_picker_ios/lib/src/messages.g.dart | 74 +++++- .../image_picker_ios/pigeons/messages.dart | 19 ++ .../image_picker_ios/pubspec.yaml | 4 +- .../test/image_picker_ios_test.dart | 234 ++++++++++++++++++ .../image_picker_ios/test/test_api.g.dart | 36 ++- .../image_picker_linux/CHANGELOG.md | 4 + .../image_picker_linux/example/lib/main.dart | 200 +++++++++++---- .../image_picker_linux/example/pubspec.yaml | 3 +- .../lib/image_picker_linux.dart | 23 ++ .../image_picker_linux/pubspec.yaml | 4 +- .../test/image_picker_linux_test.dart | 32 +++ .../image_picker_macos/CHANGELOG.md | 4 + .../image_picker_macos/example/lib/main.dart | 200 +++++++++++---- .../image_picker_macos/example/pubspec.yaml | 3 +- .../lib/image_picker_macos.dart | 24 ++ .../image_picker_macos/pubspec.yaml | 4 +- .../test/image_picker_macos_test.dart | 32 +++ .../image_picker_windows/CHANGELOG.md | 4 + .../example/lib/main.dart | 200 +++++++++++---- .../image_picker_windows/example/pubspec.yaml | 3 +- .../lib/image_picker_windows.dart | 24 ++ .../image_picker_windows/pubspec.yaml | 4 +- .../test/image_picker_windows_test.dart | 35 +++ script/configs/allowed_unpinned_deps.yaml | 1 + 55 files changed, 2559 insertions(+), 447 deletions(-) diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index 97ccd9d57ca1..971cfe08f334 100644 --- a/packages/image_picker/image_picker_android/CHANGELOG.md +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.7 + +* Adds `getMedia` method. + ## 0.8.6+20 * Bumps androidx.activity:activity from 1.7.0 to 1.7.1. diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index d216309d5888..685534ec6ab8 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -79,6 +79,7 @@ public class ImagePickerDelegate @VisibleForTesting static final int REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA = 2343; @VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345; @VisibleForTesting static final int REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY = 2346; + @VisibleForTesting static final int REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY = 2347; @VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352; @VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353; @@ -279,6 +280,52 @@ Messages.CacheRetrievalResult retrieveLostImage() { return result.build(); } + public void chooseMediaFromGallery( + @NonNull Messages.MediaSelectionOptions options, + @NonNull Messages.GeneralOptions generalOptions, + @NonNull Messages.Result> result) { + if (!setPendingOptionsAndResult(options.getImageSelectionOptions(), null, result)) { + finishWithAlreadyActiveError(result); + return; + } + + launchPickMediaFromGalleryIntent(generalOptions); + } + + private void launchPickMediaFromGalleryIntent(Messages.GeneralOptions generalOptions) { + Intent pickMediaIntent; + if (generalOptions.getUsePhotoPicker() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (generalOptions.getAllowMultiple()) { + pickMediaIntent = + new ActivityResultContracts.PickMultipleVisualMedia() + .createIntent( + activity, + new PickVisualMediaRequest.Builder() + .setMediaType( + ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE) + .build()); + } else { + pickMediaIntent = + new ActivityResultContracts.PickVisualMedia() + .createIntent( + activity, + new PickVisualMediaRequest.Builder() + .setMediaType( + ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE) + .build()); + } + } else { + pickMediaIntent = new Intent(Intent.ACTION_GET_CONTENT); + pickMediaIntent.setType("*/*"); + String[] mimeTypes = {"video/*", "image/*"}; + pickMediaIntent.putExtra("CONTENT_TYPE", mimeTypes); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + pickMediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, generalOptions.getAllowMultiple()); + } + } + activity.startActivityForResult(pickMediaIntent, REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY); + } + public void chooseVideoFromGallery( @NonNull VideoSelectionOptions options, boolean usePhotoPicker, @@ -291,9 +338,9 @@ public void chooseVideoFromGallery( launchPickVideoFromGalleryIntent(usePhotoPicker); } - private void launchPickVideoFromGalleryIntent(Boolean useAndroidPhotoPicker) { + private void launchPickVideoFromGalleryIntent(Boolean usePhotoPicker) { Intent pickVideoIntent; - if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { pickVideoIntent = new ActivityResultContracts.PickVisualMedia() .createIntent( @@ -389,9 +436,9 @@ public void chooseMultiImageFromGallery( launchMultiPickImageFromGalleryIntent(usePhotoPicker); } - private void launchPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker) { + private void launchPickImageFromGalleryIntent(Boolean usePhotoPicker) { Intent pickImageIntent; - if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { pickImageIntent = new ActivityResultContracts.PickVisualMedia() .createIntent( @@ -406,9 +453,9 @@ private void launchPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker) { activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY); } - private void launchMultiPickImageFromGalleryIntent(Boolean useAndroidPhotoPicker) { + private void launchMultiPickImageFromGalleryIntent(Boolean usePhotoPicker) { Intent pickMultiImageIntent; - if (useAndroidPhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (usePhotoPicker && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { pickMultiImageIntent = new ActivityResultContracts.PickMultipleVisualMedia() .createIntent( @@ -563,6 +610,9 @@ public boolean onActivityResult( case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA: handlerRunnable = () -> handleCaptureImageResult(resultCode); break; + case REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY: + handlerRunnable = () -> handleChooseMediaResult(resultCode, data); + break; case REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY: handlerRunnable = () -> handleChooseVideoResult(resultCode, data); break; @@ -589,17 +639,59 @@ private void handleChooseImageResult(int resultCode, Intent data) { finishWithSuccess(null); } + public class MediaPath { + public MediaPath(@NonNull String path, @Nullable String mimeType) { + this.path = path; + this.mimeType = mimeType; + } + + final String path; + final String mimeType; + + public @NonNull String getPath() { + return path; + } + + public @Nullable String getMimeType() { + return mimeType; + } + } + + private void handleChooseMediaResult(int resultCode, Intent intent) { + if (resultCode == Activity.RESULT_OK && intent != null) { + ArrayList paths = new ArrayList<>(); + if (intent.getClipData() != null) { + for (int i = 0; i < intent.getClipData().getItemCount(); i++) { + Uri uri = intent.getClipData().getItemAt(i).getUri(); + String path = fileUtils.getPathFromUri(activity, uri); + String mimeType = activity.getContentResolver().getType(uri); + paths.add(new MediaPath(path, mimeType)); + } + } else { + paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null)); + } + handleMediaResult(paths); + return; + } + + // User cancelled choosing a picture. + finishWithSuccess(null); + } + private void handleChooseMultiImageResult(int resultCode, Intent intent) { if (resultCode == Activity.RESULT_OK && intent != null) { - ArrayList paths = new ArrayList<>(); + ArrayList paths = new ArrayList<>(); if (intent.getClipData() != null) { for (int i = 0; i < intent.getClipData().getItemCount(); i++) { - paths.add(fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri())); + paths.add( + new MediaPath( + fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri()), + null)); } } else { - paths.add(fileUtils.getPathFromUri(activity, intent.getData())); + paths.add(new MediaPath(fileUtils.getPathFromUri(activity, intent.getData()), null)); } - handleMultiImageResult(paths); + handleMediaResult(paths); return; } @@ -649,26 +741,6 @@ private void handleCaptureVideoResult(int resultCode) { finishWithSuccess(null); } - private void handleMultiImageResult(ArrayList paths) { - ImageSelectionOptions localImageOptions = null; - synchronized (pendingCallStateLock) { - if (pendingCallState != null) { - localImageOptions = pendingCallState.imageOptions; - } - } - - if (localImageOptions != null) { - ArrayList finalPath = new ArrayList<>(); - for (int i = 0; i < paths.size(); i++) { - String finalImagePath = getResizedImagePath(paths.get(i), localImageOptions); - finalPath.add(i, finalImagePath); - } - finishWithListSuccess(finalPath); - } else { - finishWithListSuccess(paths); - } - } - void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) { ImageSelectionOptions localImageOptions = null; synchronized (pendingCallStateLock) { @@ -679,7 +751,7 @@ void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) { if (localImageOptions != null) { String finalImagePath = getResizedImagePath(path, localImageOptions); - //delete original file if scaled + // Delete original file if scaled. if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) { new File(path).delete(); } @@ -697,7 +769,34 @@ private String getResizedImagePath(String path, @NonNull ImageSelectionOptions o outputOptions.getQuality().intValue()); } - void handleVideoResult(String path) { + private void handleMediaResult(@NonNull ArrayList paths) { + ImageSelectionOptions localImageOptions = null; + synchronized (pendingCallStateLock) { + if (pendingCallState != null) { + localImageOptions = pendingCallState.imageOptions; + } + } + + ArrayList finalPaths = new ArrayList<>(); + if (localImageOptions != null) { + for (int i = 0; i < paths.size(); i++) { + MediaPath path = paths.get(i); + String finalPath = path.path; + if (path.mimeType == null || !path.mimeType.startsWith("video/")) { + finalPath = getResizedImagePath(path.path, localImageOptions); + } + finalPaths.add(finalPath); + } + finishWithListSuccess(finalPaths); + } else { + for (int i = 0; i < paths.size(); i++) { + finalPaths.add(paths.get(i).path); + } + finishWithListSuccess(finalPaths); + } + } + + private void handleVideoResult(String path) { finishWithSuccess(path); } diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java index 31b2303a37cb..b5deb289341a 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java @@ -19,10 +19,16 @@ import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugins.imagepicker.Messages.CacheRetrievalResult; import io.flutter.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.GeneralOptions; import io.flutter.plugins.imagepicker.Messages.ImagePickerApi; +import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions; +import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions; import io.flutter.plugins.imagepicker.Messages.Result; +import io.flutter.plugins.imagepicker.Messages.SourceCamera; import io.flutter.plugins.imagepicker.Messages.SourceSpecification; +import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions; import java.util.List; @SuppressWarnings("deprecation") @@ -279,7 +285,7 @@ final ImagePickerDelegate constructDelegate(final Activity setupActivity) { private void setCameraDevice( @NonNull ImagePickerDelegate delegate, @NonNull SourceSpecification source) { - Messages.SourceCamera camera = source.getCamera(); + SourceCamera camera = source.getCamera(); if (camera != null) { ImagePickerDelegate.CameraDevice device; switch (camera) { @@ -298,9 +304,8 @@ private void setCameraDevice( @Override public void pickImages( @NonNull SourceSpecification source, - @NonNull Messages.ImageSelectionOptions options, - @NonNull Boolean allowMultiple, - @NonNull Boolean usePhotoPicker, + @NonNull ImageSelectionOptions options, + @NonNull GeneralOptions generalOptions, @NonNull Result> result) { ImagePickerDelegate delegate = getImagePickerDelegate(); if (delegate == null) { @@ -311,12 +316,12 @@ public void pickImages( } setCameraDevice(delegate, source); - if (allowMultiple) { - delegate.chooseMultiImageFromGallery(options, usePhotoPicker, result); + if (generalOptions.getAllowMultiple()) { + delegate.chooseMultiImageFromGallery(options, generalOptions.getUsePhotoPicker(), result); } else { switch (source.getType()) { case GALLERY: - delegate.chooseImageFromGallery(options, usePhotoPicker, result); + delegate.chooseImageFromGallery(options, generalOptions.getUsePhotoPicker(), result); break; case CAMERA: delegate.takeImageWithCamera(options, result); @@ -325,12 +330,26 @@ public void pickImages( } } + @Override + public void pickMedia( + @NonNull MediaSelectionOptions mediaSelectionOptions, + @NonNull GeneralOptions generalOptions, + @NonNull Result> result) { + ImagePickerDelegate delegate = getImagePickerDelegate(); + if (delegate == null) { + result.error( + new FlutterError( + "no_activity", "image_picker plugin requires a foreground activity.", null)); + return; + } + delegate.chooseMediaFromGallery(mediaSelectionOptions, generalOptions, result); + } + @Override public void pickVideos( @NonNull SourceSpecification source, - @NonNull Messages.VideoSelectionOptions options, - @NonNull Boolean allowMultiple, - @NonNull Boolean usePhotoPicker, + @NonNull VideoSelectionOptions options, + @NonNull GeneralOptions generalOptions, @NonNull Result> result) { ImagePickerDelegate delegate = getImagePickerDelegate(); if (delegate == null) { @@ -341,12 +360,12 @@ public void pickVideos( } setCameraDevice(delegate, source); - if (allowMultiple) { + if (generalOptions.getAllowMultiple()) { result.error(new RuntimeException("Multi-video selection is not implemented")); } else { switch (source.getType()) { case GALLERY: - delegate.chooseVideoFromGallery(options, usePhotoPicker, result); + delegate.chooseVideoFromGallery(options, generalOptions.getUsePhotoPicker(), result); break; case CAMERA: delegate.takeVideoWithCamera(options, result); @@ -357,7 +376,7 @@ public void pickVideos( @Nullable @Override - public Messages.CacheRetrievalResult retrieveLostResults() { + public CacheRetrievalResult retrieveLostResults() { ImagePickerDelegate delegate = getImagePickerDelegate(); if (delegate == null) { throw new FlutterError( diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java index 17390ac6961c..8a19cfd3c55a 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/Messages.java @@ -88,6 +88,79 @@ private CacheRetrievalType(final int index) { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class GeneralOptions { + private @NonNull Boolean allowMultiple; + + public @NonNull Boolean getAllowMultiple() { + return allowMultiple; + } + + public void setAllowMultiple(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"allowMultiple\" is null."); + } + this.allowMultiple = setterArg; + } + + private @NonNull Boolean usePhotoPicker; + + public @NonNull Boolean getUsePhotoPicker() { + return usePhotoPicker; + } + + public void setUsePhotoPicker(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"usePhotoPicker\" is null."); + } + this.usePhotoPicker = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + GeneralOptions() {} + + public static final class Builder { + + private @Nullable Boolean allowMultiple; + + public @NonNull Builder setAllowMultiple(@NonNull Boolean setterArg) { + this.allowMultiple = setterArg; + return this; + } + + private @Nullable Boolean usePhotoPicker; + + public @NonNull Builder setUsePhotoPicker(@NonNull Boolean setterArg) { + this.usePhotoPicker = setterArg; + return this; + } + + public @NonNull GeneralOptions build() { + GeneralOptions pigeonReturn = new GeneralOptions(); + pigeonReturn.setAllowMultiple(allowMultiple); + pigeonReturn.setUsePhotoPicker(usePhotoPicker); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(allowMultiple); + toListResult.add(usePhotoPicker); + return toListResult; + } + + static @NonNull GeneralOptions fromList(@NonNull ArrayList list) { + GeneralOptions pigeonResult = new GeneralOptions(); + Object allowMultiple = list.get(0); + pigeonResult.setAllowMultiple((Boolean) allowMultiple); + Object usePhotoPicker = list.get(1); + pigeonResult.setUsePhotoPicker((Boolean) usePhotoPicker); + return pigeonResult; + } + } + /** * Options for image selection and output. * @@ -193,6 +266,58 @@ ArrayList toList() { } } + /** Generated class from Pigeon that represents data sent in messages. */ + public static final class MediaSelectionOptions { + private @NonNull ImageSelectionOptions imageSelectionOptions; + + public @NonNull ImageSelectionOptions getImageSelectionOptions() { + return imageSelectionOptions; + } + + public void setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"imageSelectionOptions\" is null."); + } + this.imageSelectionOptions = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + MediaSelectionOptions() {} + + public static final class Builder { + + private @Nullable ImageSelectionOptions imageSelectionOptions; + + public @NonNull Builder setImageSelectionOptions(@NonNull ImageSelectionOptions setterArg) { + this.imageSelectionOptions = setterArg; + return this; + } + + public @NonNull MediaSelectionOptions build() { + MediaSelectionOptions pigeonReturn = new MediaSelectionOptions(); + pigeonReturn.setImageSelectionOptions(imageSelectionOptions); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add((imageSelectionOptions == null) ? null : imageSelectionOptions.toList()); + return toListResult; + } + + static @NonNull MediaSelectionOptions fromList(@NonNull ArrayList list) { + MediaSelectionOptions pigeonResult = new MediaSelectionOptions(); + Object imageSelectionOptions = list.get(0); + pigeonResult.setImageSelectionOptions( + (imageSelectionOptions == null) + ? null + : ImageSelectionOptions.fromList((ArrayList) imageSelectionOptions)); + return pigeonResult; + } + } + /** * Options for image selection and output. * @@ -523,10 +648,14 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { case (byte) 129: return CacheRetrievalResult.fromList((ArrayList) readValue(buffer)); case (byte) 130: - return ImageSelectionOptions.fromList((ArrayList) readValue(buffer)); + return GeneralOptions.fromList((ArrayList) readValue(buffer)); case (byte) 131: - return SourceSpecification.fromList((ArrayList) readValue(buffer)); + return ImageSelectionOptions.fromList((ArrayList) readValue(buffer)); case (byte) 132: + return MediaSelectionOptions.fromList((ArrayList) readValue(buffer)); + case (byte) 133: + return SourceSpecification.fromList((ArrayList) readValue(buffer)); + case (byte) 134: return VideoSelectionOptions.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -541,14 +670,20 @@ protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { } else if (value instanceof CacheRetrievalResult) { stream.write(129); writeValue(stream, ((CacheRetrievalResult) value).toList()); - } else if (value instanceof ImageSelectionOptions) { + } else if (value instanceof GeneralOptions) { stream.write(130); + writeValue(stream, ((GeneralOptions) value).toList()); + } else if (value instanceof ImageSelectionOptions) { + stream.write(131); writeValue(stream, ((ImageSelectionOptions) value).toList()); + } else if (value instanceof MediaSelectionOptions) { + stream.write(132); + writeValue(stream, ((MediaSelectionOptions) value).toList()); } else if (value instanceof SourceSpecification) { - stream.write(131); + stream.write(133); writeValue(stream, ((SourceSpecification) value).toList()); } else if (value instanceof VideoSelectionOptions) { - stream.write(132); + stream.write(134); writeValue(stream, ((VideoSelectionOptions) value).toList()); } else { super.writeValue(stream, value); @@ -567,8 +702,7 @@ public interface ImagePickerApi { void pickImages( @NonNull SourceSpecification source, @NonNull ImageSelectionOptions options, - @NonNull Boolean allowMultiple, - @NonNull Boolean usePhotoPicker, + @NonNull GeneralOptions generalOptions, @NonNull Result> result); /** * Selects video and returns their paths. @@ -579,8 +713,17 @@ void pickImages( void pickVideos( @NonNull SourceSpecification source, @NonNull VideoSelectionOptions options, - @NonNull Boolean allowMultiple, - @NonNull Boolean usePhotoPicker, + @NonNull GeneralOptions generalOptions, + @NonNull Result> result); + /** + * Selects images and videos and returns their paths. + * + *

Elements must not be null, by convention. See + * https://github.com/flutter/flutter/issues/97848 + */ + void pickMedia( + @NonNull MediaSelectionOptions mediaSelectionOptions, + @NonNull GeneralOptions generalOptions, @NonNull Result> result); /** Returns results from a previous app session, if any. */ @Nullable @@ -607,8 +750,7 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ImagePicke ArrayList args = (ArrayList) message; SourceSpecification sourceArg = (SourceSpecification) args.get(0); ImageSelectionOptions optionsArg = (ImageSelectionOptions) args.get(1); - Boolean allowMultipleArg = (Boolean) args.get(2); - Boolean usePhotoPickerArg = (Boolean) args.get(3); + GeneralOptions generalOptionsArg = (GeneralOptions) args.get(2); Result> resultCallback = new Result>() { public void success(List result) { @@ -622,8 +764,7 @@ public void error(Throwable error) { } }; - api.pickImages( - sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback); + api.pickImages(sourceArg, optionsArg, generalOptionsArg, resultCallback); }); } else { channel.setMessageHandler(null); @@ -644,8 +785,38 @@ public void error(Throwable error) { ArrayList args = (ArrayList) message; SourceSpecification sourceArg = (SourceSpecification) args.get(0); VideoSelectionOptions optionsArg = (VideoSelectionOptions) args.get(1); - Boolean allowMultipleArg = (Boolean) args.get(2); - Boolean usePhotoPickerArg = (Boolean) args.get(3); + GeneralOptions generalOptionsArg = (GeneralOptions) args.get(2); + Result> resultCallback = + new Result>() { + public void success(List result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.pickVideos(sourceArg, optionsArg, generalOptionsArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.ImagePickerApi.pickMedia", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + MediaSelectionOptions mediaSelectionOptionsArg = + (MediaSelectionOptions) args.get(0); + GeneralOptions generalOptionsArg = (GeneralOptions) args.get(1); Result> resultCallback = new Result>() { public void success(List result) { @@ -659,8 +830,7 @@ public void error(Throwable error) { } }; - api.pickVideos( - sourceArg, optionsArg, allowMultipleArg, usePhotoPickerArg, resultCallback); + api.pickMedia(mediaSelectionOptionsArg, generalOptionsArg, resultCallback); }); } else { channel.setMessageHandler(null); diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index efdbbae3b7f9..73ee5a0f0d49 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -29,7 +29,9 @@ import android.net.Uri; import androidx.annotation.Nullable; import io.flutter.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.GeneralOptions; import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions; +import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions; import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions; import java.io.File; import java.io.IOException; @@ -61,6 +63,8 @@ public class ImagePickerDelegateTest { new ImageSelectionOptions.Builder().setQuality((long) 100).setMaxWidth(WIDTH).build(); private static final VideoSelectionOptions DEFAULT_VIDEO_OPTIONS = new VideoSelectionOptions.Builder().build(); + private static final MediaSelectionOptions DEFAULT_MEDIA_OPTIONS = + new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build(); @Mock Activity mockActivity; @Mock ImageResizer mockImageResizer; @@ -161,6 +165,18 @@ public void chooseMultiImageFromGallery_whenPendingResultExists_finishesWithAlre verifyNoMoreInteractions(mockResult); } + @Test + public void chooseMediaFromGallery_whenPendingResultExists_finishesWithAlreadyActiveError() { + ImagePickerDelegate delegate = + createDelegateWithPendingResultAndOptions(DEFAULT_IMAGE_OPTIONS, null); + GeneralOptions generalOptions = + new GeneralOptions.Builder().setAllowMultiple(true).setUsePhotoPicker(true).build(); + delegate.chooseMediaFromGallery(DEFAULT_MEDIA_OPTIONS, generalOptions, mockResult); + + verifyFinishedWithAlreadyActiveError(); + verifyNoMoreInteractions(mockResult); + } + @Test @Config(sdk = 30) public void chooseImageFromGallery_launchesChooseFromGalleryIntent() { @@ -631,6 +647,19 @@ public void onActivityResult_whenMultipleImagesPickedFromGallery_returnsTrue() { assertTrue(isHandled); } + @Test + public void onActivityResult_whenMediaPickedFromGallery_returnsTrue() { + ImagePickerDelegate delegate = createDelegate(); + + boolean isHandled = + delegate.onActivityResult( + ImagePickerDelegate.REQUEST_CODE_CHOOSE_MEDIA_FROM_GALLERY, + Activity.RESULT_OK, + mockIntent); + + assertTrue(isHandled); + } + @Test public void onActivityResult_whenVideoPickerFromGallery_returnsTrue() { ImagePickerDelegate delegate = createDelegate(); diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java index cd408c5cef43..b2c281ca540d 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java @@ -24,7 +24,9 @@ import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.imagepicker.Messages.FlutterError; +import io.flutter.plugins.imagepicker.Messages.GeneralOptions; import io.flutter.plugins.imagepicker.Messages.ImageSelectionOptions; +import io.flutter.plugins.imagepicker.Messages.MediaSelectionOptions; import io.flutter.plugins.imagepicker.Messages.SourceSpecification; import io.flutter.plugins.imagepicker.Messages.VideoSelectionOptions; import java.util.List; @@ -40,6 +42,16 @@ public class ImagePickerPluginTest { new ImageSelectionOptions.Builder().setQuality((long) 100).build(); private static final VideoSelectionOptions DEFAULT_VIDEO_OPTIONS = new VideoSelectionOptions.Builder().build(); + private static final MediaSelectionOptions DEFAULT_MEDIA_OPTIONS = + new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build(); + private static final GeneralOptions GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER = + new GeneralOptions.Builder().setUsePhotoPicker(true).setAllowMultiple(true).build(); + private static final GeneralOptions GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_USE_PHOTO_PICKER = + new GeneralOptions.Builder().setUsePhotoPicker(true).setAllowMultiple(false).build(); + private static final GeneralOptions GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER = + new GeneralOptions.Builder().setUsePhotoPicker(false).setAllowMultiple(false).build(); + private static final GeneralOptions GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER = + new GeneralOptions.Builder().setUsePhotoPicker(false).setAllowMultiple(true).build(); private static final SourceSpecification SOURCE_GALLERY = new SourceSpecification.Builder().setType(Messages.SourceType.GALLERY).build(); private static final SourceSpecification SOURCE_CAMERA_FRONT = @@ -88,7 +100,10 @@ public void pickImages_whenActivityIsNull_finishesWithForegroundActivityRequired ImagePickerPlugin imagePickerPluginWithNullActivity = new ImagePickerPlugin(mockImagePickerDelegate, null); imagePickerPluginWithNullActivity.pickImages( - SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER, + mockResult); ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); verify(mockResult).error(errorCaptor.capture()); @@ -103,7 +118,10 @@ public void pickVideos_whenActivityIsNull_finishesWithForegroundActivityRequired ImagePickerPlugin imagePickerPluginWithNullActivity = new ImagePickerPlugin(mockImagePickerDelegate, null); imagePickerPluginWithNullActivity.pickVideos( - SOURCE_CAMERA_REAR, DEFAULT_VIDEO_OPTIONS, false, false, mockResult); + SOURCE_CAMERA_REAR, + DEFAULT_VIDEO_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); ArgumentCaptor errorCaptor = ArgumentCaptor.forClass(FlutterError.class); verify(mockResult).error(errorCaptor.capture()); @@ -126,60 +144,126 @@ public void retrieveLostResults_whenActivityIsNull_finishesWithForegroundActivit @Test public void pickImages_whenSourceIsGallery_invokesChooseImageFromGallery() { - plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).chooseImageFromGallery(any(), eq(false), any()); verifyNoInteractions(mockResult); } @Test public void pickImages_whenSourceIsGalleryUsingPhotoPicker_invokesChooseImageFromGallery() { - plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, false, true, mockResult); + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).chooseImageFromGallery(any(), eq(true), any()); verifyNoInteractions(mockResult); } @Test public void pickImages_invokesChooseMultiImageFromGallery() { - plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, true, false, mockResult); + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(false), any()); verifyNoInteractions(mockResult); } @Test public void pickImages_usingPhotoPicker_invokesChooseMultiImageFromGallery() { - plugin.pickImages(SOURCE_GALLERY, DEFAULT_IMAGE_OPTIONS, true, true, mockResult); + plugin.pickImages( + SOURCE_GALLERY, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_ALLOW_MULTIPLE_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).chooseMultiImageFromGallery(any(), eq(true), any()); verifyNoInteractions(mockResult); } + @Test + public void pickMedia_invokesChooseMediaFromGallery() { + MediaSelectionOptions mediaSelectionOptions = + new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build(); + plugin.pickMedia( + mediaSelectionOptions, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); + verify(mockImagePickerDelegate) + .chooseMediaFromGallery( + eq(mediaSelectionOptions), + eq(GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER), + any()); + verifyNoInteractions(mockResult); + } + + @Test + public void pickMedia_usingPhotoPicker_invokesChooseMediaFromGallery() { + MediaSelectionOptions mediaSelectionOptions = + new MediaSelectionOptions.Builder().setImageSelectionOptions(DEFAULT_IMAGE_OPTIONS).build(); + plugin.pickMedia( + mediaSelectionOptions, GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, mockResult); + verify(mockImagePickerDelegate) + .chooseMediaFromGallery( + eq(mediaSelectionOptions), + eq(GENERAL_OPTIONS_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER), + any()); + verifyNoInteractions(mockResult); + } + @Test public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera() { - plugin.pickImages(SOURCE_CAMERA_REAR, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + plugin.pickImages( + SOURCE_CAMERA_REAR, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).takeImageWithCamera(any(), any()); verifyNoInteractions(mockResult); } @Test public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() { - plugin.pickImages(SOURCE_CAMERA_REAR, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + plugin.pickImages( + SOURCE_CAMERA_REAR, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.REAR)); } @Test public void pickImages_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() { - plugin.pickImages(SOURCE_CAMERA_FRONT, DEFAULT_IMAGE_OPTIONS, false, false, mockResult); + plugin.pickImages( + SOURCE_CAMERA_FRONT, + DEFAULT_IMAGE_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT)); } @Test public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_RearCamera() { - plugin.pickVideos(SOURCE_CAMERA_REAR, DEFAULT_VIDEO_OPTIONS, false, false, mockResult); + plugin.pickVideos( + SOURCE_CAMERA_REAR, + DEFAULT_VIDEO_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.REAR)); } @Test public void pickVideos_whenSourceIsCamera_invokesTakeImageWithCamera_FrontCamera() { - plugin.pickVideos(SOURCE_CAMERA_FRONT, DEFAULT_VIDEO_OPTIONS, false, false, mockResult); + plugin.pickVideos( + SOURCE_CAMERA_FRONT, + DEFAULT_VIDEO_OPTIONS, + GENERAL_OPTIONS_DONT_ALLOW_MULTIPLE_DONT_USE_PHOTO_PICKER, + mockResult); verify(mockImagePickerDelegate).setCameraDevice(eq(ImagePickerDelegate.CameraDevice.FRONT)); } diff --git a/packages/image_picker/image_picker_android/example/lib/main.dart b/packages/image_picker/image_picker_android/example/lib/main.dart index fa87587857b0..7d58a2a69074 100755 --- a/packages/image_picker/image_picker_android/example/lib/main.dart +++ b/packages/image_picker/image_picker_android/example/lib/main.dart @@ -14,6 +14,7 @@ import 'package:flutter_driver/driver_extension.dart'; import 'package:image_picker_android/image_picker_android.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; // #enddocregion photo-picker-example +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void appMain() { @@ -55,14 +56,14 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; - bool isVideo = false; + bool _isVideo = false; VideoPlayerController? _controller; VideoPlayerController? _toBeDisposed; @@ -77,18 +78,10 @@ class _MyHomePageState extends State { if (file != null && mounted) { await _disposeVideoController(); late VideoPlayerController controller; - if (kIsWeb) { - controller = VideoPlayerController.network(file.path); - } else { - controller = VideoPlayerController.file(File(file.path)); - } + + controller = VideoPlayerController.file(File(file.path)); _controller = controller; - // In web, most browsers won't honor a programmatic call to .play - // if the video has a sound track (and is not muted). - // Mute the video so it auto-plays in web! - // This is not needed if the call to .play is the result of user - // interaction (clicking on a "play" button, for example). - const double volume = kIsWeb ? 0.0 : 1.0; + const double volume = 1.0; await controller.setVolume(volume); await controller.initialize(); await controller.setLooping(true); @@ -101,12 +94,13 @@ class _MyHomePageState extends State { ImageSource source, { required BuildContext context, bool isMultiImage = false, + bool isMedia = false, }) async { if (_controller != null) { await _controller!.setVolume(0.0); } if (context.mounted) { - if (isVideo) { + if (_isVideo) { final XFile? file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); if (file != null && context.mounted) { @@ -117,15 +111,54 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { - final List? pickedFileList = await _picker.getMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); + final List? pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); if (pickedFileList != null && context.mounted) { _showPickedSnackBar(context, pickedFileList); } - setState(() => _imageFileList = pickedFileList); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } } catch (e) { setState(() => _pickImageError = e); } @@ -200,30 +233,37 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { - final XFile image = _imageFileList![index]; + final XFile image = _mediaFileList![index]; + final String? mime = lookupMimeType(_mediaFileList![index].path); return Column( mainAxisSize: MainAxisSize.min, children: [ Text(image.name, key: const Key('image_picker_example_picked_image_name')), - // Why network for web? - // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform Semantics( label: 'image_picker_example_picked_image', - child: kIsWeb - ? Image.network(image.path) - : Image.file(File(image.path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: + Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ), ], ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -239,8 +279,19 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { - if (isVideo) { + if (_isVideo) { return _previewVideo(); } else { return _previewImages(); @@ -254,15 +305,15 @@ class _MyHomePageState extends State { } if (response.file != null) { if (response.type == RetrieveType.video) { - isVideo = true; + _isVideo = true; await _playVideo(response.file); } else { - isVideo = false; + _isVideo = false; setState(() { if (response.files == null) { _setImageFileListFromFile(response.file); } else { - _imageFileList = response.files; + _mediaFileList = response.files; } }); } @@ -316,7 +367,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( key: const Key('image_picker_example_from_gallery'), onPressed: () { - isVideo = false; + _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'image0', @@ -328,7 +379,40 @@ class _MyHomePageState extends State { padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { - isVideo = false; + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; _onImageButtonPressed( ImageSource.gallery, context: context, @@ -344,7 +428,7 @@ class _MyHomePageState extends State { padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { - isVideo = false; + _isVideo = false; _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'image2', @@ -357,7 +441,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { - isVideo = true; + _isVideo = true; _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'video0', @@ -370,7 +454,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { - isVideo = true; + _isVideo = true; _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'video1', @@ -510,3 +594,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_android/example/pubspec.yaml b/packages/image_picker/image_picker_android/example/pubspec.yaml index 8921a37a67e9..1cce21a99e7d 100644 --- a/packages/image_picker/image_picker_android/example/pubspec.yaml +++ b/packages/image_picker/image_picker_android/example/pubspec.yaml @@ -19,7 +19,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - image_picker_platform_interface: ^2.3.0 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker_android/lib/image_picker_android.dart b/packages/image_picker/image_picker_android/lib/image_picker_android.dart index fbc7fa7c2ad2..c9e2c875e8b5 100644 --- a/packages/image_picker/image_picker_android/lib/image_picker_android.dart +++ b/packages/image_picker/image_picker_android/lib/image_picker_android.dart @@ -11,12 +11,17 @@ import 'src/messages.g.dart'; /// An Android implementation of [ImagePickerPlatform]. class ImagePickerAndroid extends ImagePickerPlatform { - /// Creates a new plugin implemenation instance. + /// Creates a new plugin implementation instance. ImagePickerAndroid({@visibleForTesting ImagePickerApi? api}) : _hostApi = api ?? ImagePickerApi(); final ImagePickerApi _hostApi; + /// Sets [ImagePickerAndroid] to use Android 13 Photo Picker. + /// + /// Currently defaults to false, but the default is subject to change. + bool useAndroidPhotoPicker = false; + /// Registers this class as the default platform implementation. static void registerWith() { ImagePickerPlatform.instance = ImagePickerAndroid(); @@ -77,13 +82,14 @@ class ImagePickerAndroid extends ImagePickerPlatform { } return _hostApi.pickImages( - SourceSpecification(type: SourceType.gallery), - ImageSelectionOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - quality: imageQuality ?? 100), - /* allowMultiple */ true, - useAndroidPhotoPicker); + SourceSpecification(type: SourceType.gallery), + ImageSelectionOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + quality: imageQuality ?? 100), + GeneralOptions( + allowMultiple: true, usePhotoPicker: useAndroidPhotoPicker), + ); } Future _getImagePath({ @@ -108,13 +114,16 @@ class ImagePickerAndroid extends ImagePickerPlatform { } final List paths = await _hostApi.pickImages( - _buildSourceSpec(source, preferredCameraDevice), - ImageSelectionOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - quality: imageQuality ?? 100), - /* allowMultiple */ false, - useAndroidPhotoPicker); + _buildSourceSpec(source, preferredCameraDevice), + ImageSelectionOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + quality: imageQuality ?? 100), + GeneralOptions( + allowMultiple: false, + usePhotoPicker: useAndroidPhotoPicker, + ), + ); return paths.isEmpty ? null : paths.first; } @@ -138,10 +147,13 @@ class ImagePickerAndroid extends ImagePickerPlatform { Duration? maxDuration, }) async { final List paths = await _hostApi.pickVideos( - _buildSourceSpec(source, preferredCameraDevice), - VideoSelectionOptions(maxDurationSeconds: maxDuration?.inSeconds), - /* allowMultiple */ false, - useAndroidPhotoPicker); + _buildSourceSpec(source, preferredCameraDevice), + VideoSelectionOptions(maxDurationSeconds: maxDuration?.inSeconds), + GeneralOptions( + allowMultiple: false, + usePhotoPicker: useAndroidPhotoPicker, + ), + ); return paths.isEmpty ? null : paths.first; } @@ -197,6 +209,21 @@ class ImagePickerAndroid extends ImagePickerPlatform { return paths.map((dynamic path) => XFile(path as String)).toList(); } + @override + Future> getMedia({ + required MediaOptions options, + }) async { + return (await _hostApi.pickMedia( + _mediaOptionsToMediaSelectionOptions(options), + GeneralOptions( + allowMultiple: options.allowMultiple, + usePhotoPicker: useAndroidPhotoPicker, + ), + )) + .map((String? path) => XFile(path!)) + .toList(); + } + @override Future getVideo({ required ImageSource source, @@ -211,6 +238,38 @@ class ImagePickerAndroid extends ImagePickerPlatform { return path != null ? XFile(path) : null; } + MediaSelectionOptions _mediaOptionsToMediaSelectionOptions( + MediaOptions mediaOptions) { + final ImageSelectionOptions imageSelectionOptions = + _imageOptionsToImageSelectionOptionsWithValidator( + mediaOptions.imageOptions); + return MediaSelectionOptions( + imageSelectionOptions: imageSelectionOptions, + ); + } + + ImageSelectionOptions _imageOptionsToImageSelectionOptionsWithValidator( + ImageOptions? imageOptions) { + final double? maxHeight = imageOptions?.maxHeight; + final double? maxWidth = imageOptions?.maxWidth; + final int? imageQuality = imageOptions?.imageQuality; + + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + return ImageSelectionOptions( + quality: imageQuality ?? 100, maxHeight: maxHeight, maxWidth: maxWidth); + } + @override Future retrieveLostData() async { final LostDataResponse result = await getLostData(); @@ -243,7 +302,7 @@ class ImagePickerAndroid extends ImagePickerPlatform { : PlatformException(code: error.code, message: error.message); // Entries are guaranteed not to be null, even though that's not currently - // expressable in Pigeon. + // expressible in Pigeon. final List pickedFileList = result.paths.map((String? path) => XFile(path!)).toList(); @@ -309,9 +368,4 @@ class ImagePickerAndroid extends ImagePickerPlatform { // ignore: dead_code return RetrieveType.image; } - - /// Sets [ImagePickerAndroid] to use Android 13 Photo Picker. - /// - /// Currently defaults to false, but the default is subject to change. - bool useAndroidPhotoPicker = false; } diff --git a/packages/image_picker/image_picker_android/lib/src/messages.g.dart b/packages/image_picker/image_picker_android/lib/src/messages.g.dart index a4f15c847559..476e80db001e 100644 --- a/packages/image_picker/image_picker_android/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_android/lib/src/messages.g.dart @@ -26,6 +26,32 @@ enum CacheRetrievalType { video, } +class GeneralOptions { + GeneralOptions({ + required this.allowMultiple, + required this.usePhotoPicker, + }); + + bool allowMultiple; + + bool usePhotoPicker; + + Object encode() { + return [ + allowMultiple, + usePhotoPicker, + ]; + } + + static GeneralOptions decode(Object result) { + result as List; + return GeneralOptions( + allowMultiple: result[0]! as bool, + usePhotoPicker: result[1]! as bool, + ); + } +} + /// Options for image selection and output. class ImageSelectionOptions { ImageSelectionOptions({ @@ -63,6 +89,28 @@ class ImageSelectionOptions { } } +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.imageSelectionOptions, + }); + + ImageSelectionOptions imageSelectionOptions; + + Object encode() { + return [ + imageSelectionOptions.encode(), + ]; + } + + static MediaSelectionOptions decode(Object result) { + result as List; + return MediaSelectionOptions( + imageSelectionOptions: + ImageSelectionOptions.decode(result[0]! as List), + ); + } +} + /// Options for image selection and output. class VideoSelectionOptions { VideoSelectionOptions({ @@ -192,15 +240,21 @@ class _ImagePickerApiCodec extends StandardMessageCodec { } else if (value is CacheRetrievalResult) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is ImageSelectionOptions) { + } else if (value is GeneralOptions) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is SourceSpecification) { + } else if (value is ImageSelectionOptions) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is VideoSelectionOptions) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is VideoSelectionOptions) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -214,10 +268,14 @@ class _ImagePickerApiCodec extends StandardMessageCodec { case 129: return CacheRetrievalResult.decode(readValue(buffer)!); case 130: - return ImageSelectionOptions.decode(readValue(buffer)!); + return GeneralOptions.decode(readValue(buffer)!); case 131: - return SourceSpecification.decode(readValue(buffer)!); + return ImageSelectionOptions.decode(readValue(buffer)!); case 132: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 133: + return SourceSpecification.decode(readValue(buffer)!); + case 134: return VideoSelectionOptions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -242,17 +300,13 @@ class ImagePickerApi { Future> pickImages( SourceSpecification arg_source, ImageSelectionOptions arg_options, - bool arg_allowMultiple, - bool arg_usePhotoPicker) async { + GeneralOptions arg_generalOptions) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.ImagePickerApi.pickImages', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send([ - arg_source, - arg_options, - arg_allowMultiple, - arg_usePhotoPicker - ]) as List?; + final List? replyList = await channel + .send([arg_source, arg_options, arg_generalOptions]) + as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -281,17 +335,47 @@ class ImagePickerApi { Future> pickVideos( SourceSpecification arg_source, VideoSelectionOptions arg_options, - bool arg_allowMultiple, - bool arg_usePhotoPicker) async { + GeneralOptions arg_generalOptions) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.ImagePickerApi.pickVideos', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send([ - arg_source, - arg_options, - arg_allowMultiple, - arg_usePhotoPicker - ]) as List?; + final List? replyList = await channel + .send([arg_source, arg_options, arg_generalOptions]) + as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } + + /// Selects images and videos and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + Future> pickMedia( + MediaSelectionOptions arg_mediaSelectionOptions, + GeneralOptions arg_generalOptions) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_mediaSelectionOptions, arg_generalOptions]) + as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', diff --git a/packages/image_picker/image_picker_android/pigeons/messages.dart b/packages/image_picker/image_picker_android/pigeons/messages.dart index 31ff22f1fbbe..9d264b5a11f8 100644 --- a/packages/image_picker/image_picker_android/pigeons/messages.dart +++ b/packages/image_picker/image_picker_android/pigeons/messages.dart @@ -13,6 +13,11 @@ import 'package:pigeon/pigeon.dart'; ), copyrightHeader: 'pigeons/copyright.txt', )) +class GeneralOptions { + GeneralOptions(this.allowMultiple, this.usePhotoPicker); + bool allowMultiple; + bool usePhotoPicker; +} /// Options for image selection and output. class ImageSelectionOptions { @@ -30,6 +35,14 @@ class ImageSelectionOptions { int quality; } +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.imageSelectionOptions, + }); + + ImageSelectionOptions imageSelectionOptions; +} + /// Options for image selection and output. class VideoSelectionOptions { VideoSelectionOptions({this.maxDurationSeconds}); @@ -89,8 +102,11 @@ abstract class ImagePickerApi { /// https://github.com/flutter/flutter/issues/97848 @TaskQueue(type: TaskQueueType.serialBackgroundThread) @async - List pickImages(SourceSpecification source, - ImageSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + List pickImages( + SourceSpecification source, + ImageSelectionOptions options, + GeneralOptions generalOptions, + ); /// Selects video and returns their paths. /// @@ -98,8 +114,21 @@ abstract class ImagePickerApi { /// https://github.com/flutter/flutter/issues/97848 @TaskQueue(type: TaskQueueType.serialBackgroundThread) @async - List pickVideos(SourceSpecification source, - VideoSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + List pickVideos( + SourceSpecification source, + VideoSelectionOptions options, + GeneralOptions generalOptions, + ); + + /// Selects images and videos and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + @async + List pickMedia( + MediaSelectionOptions mediaSelectionOptions, + GeneralOptions generalOptions, + ); /// Returns results from a previous app session, if any. @TaskQueue(type: TaskQueueType.serialBackgroundThread) diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index 8c61648db822..ed7c8dbd5874 100755 --- a/packages/image_picker/image_picker_android/pubspec.yaml +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -3,7 +3,7 @@ description: Android implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.6+20 +version: 0.8.7 environment: sdk: ">=2.18.0 <4.0.0" @@ -22,7 +22,7 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 - image_picker_platform_interface: ^2.5.0 + image_picker_platform_interface: ^2.8.0 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart index f17d078a9031..0b0cab4d6dfb 100644 --- a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart +++ b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart @@ -654,6 +654,129 @@ void main() { }); }); + group('#getMedia', () { + test('calls the method correctly', () async { + const List fakePaths = ['/foo.jgp', 'bar.jpg']; + api.returnValue = fakePaths; + + final List files = await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ); + + expect(api.lastCall, _LastPickType.image); + expect(files.length, 2); + expect(files[0].path, fakePaths[0]); + expect(files[1].path, fakePaths[1]); + }); + + test('passes default image options', () async { + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ); + + expect(api.passedImageOptions?.maxWidth, null); + expect(api.passedImageOptions?.maxHeight, null); + expect(api.passedImageOptions?.quality, 100); + }); + + test('passes image option arguments correctly', () async { + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + )); + + expect(api.passedImageOptions?.maxWidth, 10.0); + expect(api.passedImageOptions?.maxHeight, 20.0); + expect(api.passedImageOptions?.quality, 70); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(maxWidth: -1.0), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(maxHeight: -1.0), + ), + ), + throwsArgumentError, + ); + }); + + test('does not accept an invalid imageQuality argument', () { + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(imageQuality: -1), + ), + ), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(imageQuality: 101), + ), + ), + throwsArgumentError, + ); + }); + + test('handles an empty path response gracefully', () async { + api.returnValue = []; + + expect( + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ), + []); + }); + + test('defaults to not using Android Photo Picker', () async { + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ); + + expect(api.passedPhotoPickerFlag, false); + }); + + test('allows using Android Photo Picker', () async { + picker.useAndroidPhotoPicker = true; + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ); + + expect(api.passedPhotoPickerFlag, true); + }); + }); + group('#getImageFromSource', () { test('calls the method correctly', () async { const String fakePath = '/foo.jpg'; @@ -807,29 +930,41 @@ class _FakeImagePickerApi implements ImagePickerApi { @override Future> pickImages( - SourceSpecification source, - ImageSelectionOptions options, - bool allowMultiple, - bool usePhotoPicker) async { + SourceSpecification source, + ImageSelectionOptions options, + GeneralOptions generalOptions, + ) async { lastCall = _LastPickType.image; passedSource = source; passedImageOptions = options; - passedAllowMultiple = allowMultiple; - passedPhotoPickerFlag = usePhotoPicker; + passedAllowMultiple = generalOptions.allowMultiple; + passedPhotoPickerFlag = generalOptions.usePhotoPicker; + return returnValue as List? ?? []; + } + + @override + Future> pickMedia( + MediaSelectionOptions options, + GeneralOptions generalOptions, + ) async { + lastCall = _LastPickType.image; + passedImageOptions = options.imageSelectionOptions; + passedPhotoPickerFlag = generalOptions.usePhotoPicker; + passedAllowMultiple = generalOptions.allowMultiple; return returnValue as List? ?? []; } @override Future> pickVideos( - SourceSpecification source, - VideoSelectionOptions options, - bool allowMultiple, - bool usePhotoPicker) async { + SourceSpecification source, + VideoSelectionOptions options, + GeneralOptions generalOptions, + ) async { lastCall = _LastPickType.video; passedSource = source; passedVideoOptions = options; - passedAllowMultiple = allowMultiple; - passedPhotoPickerFlag = usePhotoPicker; + passedAllowMultiple = generalOptions.allowMultiple; + passedPhotoPickerFlag = generalOptions.usePhotoPicker; return returnValue as List? ?? []; } diff --git a/packages/image_picker/image_picker_android/test/test_api.g.dart b/packages/image_picker/image_picker_android/test/test_api.g.dart index dbb6b143a91d..d3b68913a68a 100644 --- a/packages/image_picker/image_picker_android/test/test_api.g.dart +++ b/packages/image_picker/image_picker_android/test/test_api.g.dart @@ -23,15 +23,21 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { } else if (value is CacheRetrievalResult) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is ImageSelectionOptions) { + } else if (value is GeneralOptions) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is SourceSpecification) { + } else if (value is ImageSelectionOptions) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is VideoSelectionOptions) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(132); writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is VideoSelectionOptions) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -45,10 +51,14 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { case 129: return CacheRetrievalResult.decode(readValue(buffer)!); case 130: - return ImageSelectionOptions.decode(readValue(buffer)!); + return GeneralOptions.decode(readValue(buffer)!); case 131: - return SourceSpecification.decode(readValue(buffer)!); + return ImageSelectionOptions.decode(readValue(buffer)!); case 132: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 133: + return SourceSpecification.decode(readValue(buffer)!); + case 134: return VideoSelectionOptions.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -66,14 +76,21 @@ abstract class TestHostImagePickerApi { /// Elements must not be null, by convention. See /// https://github.com/flutter/flutter/issues/97848 Future> pickImages(SourceSpecification source, - ImageSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + ImageSelectionOptions options, GeneralOptions generalOptions); /// Selects video and returns their paths. /// /// Elements must not be null, by convention. See /// https://github.com/flutter/flutter/issues/97848 Future> pickVideos(SourceSpecification source, - VideoSelectionOptions options, bool allowMultiple, bool usePhotoPicker); + VideoSelectionOptions options, GeneralOptions generalOptions); + + /// Selects images and videos and returns their paths. + /// + /// Elements must not be null, by convention. See + /// https://github.com/flutter/flutter/issues/97848 + Future> pickMedia(MediaSelectionOptions mediaSelectionOptions, + GeneralOptions generalOptions); /// Returns results from a previous app session, if any. CacheRetrievalResult? retrieveLostResults(); @@ -102,14 +119,12 @@ abstract class TestHostImagePickerApi { (args[1] as ImageSelectionOptions?); assert(arg_options != null, 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null ImageSelectionOptions.'); - final bool? arg_allowMultiple = (args[2] as bool?); - assert(arg_allowMultiple != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null bool.'); - final bool? arg_usePhotoPicker = (args[3] as bool?); - assert(arg_usePhotoPicker != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null bool.'); - final List output = await api.pickImages(arg_source!, - arg_options!, arg_allowMultiple!, arg_usePhotoPicker!); + final GeneralOptions? arg_generalOptions = + (args[2] as GeneralOptions?); + assert(arg_generalOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickImages was null, expected non-null GeneralOptions.'); + final List output = await api.pickImages( + arg_source!, arg_options!, arg_generalOptions!); return [output]; }); } @@ -136,14 +151,40 @@ abstract class TestHostImagePickerApi { (args[1] as VideoSelectionOptions?); assert(arg_options != null, 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null VideoSelectionOptions.'); - final bool? arg_allowMultiple = (args[2] as bool?); - assert(arg_allowMultiple != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null bool.'); - final bool? arg_usePhotoPicker = (args[3] as bool?); - assert(arg_usePhotoPicker != null, - 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null bool.'); - final List output = await api.pickVideos(arg_source!, - arg_options!, arg_allowMultiple!, arg_usePhotoPicker!); + final GeneralOptions? arg_generalOptions = + (args[2] as GeneralOptions?); + assert(arg_generalOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickVideos was null, expected non-null GeneralOptions.'); + final List output = await api.pickVideos( + arg_source!, arg_options!, arg_generalOptions!); + return [output]; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null.'); + final List args = (message as List?)!; + final MediaSelectionOptions? arg_mediaSelectionOptions = + (args[0] as MediaSelectionOptions?); + assert(arg_mediaSelectionOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.'); + final GeneralOptions? arg_generalOptions = + (args[1] as GeneralOptions?); + assert(arg_generalOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null GeneralOptions.'); + final List output = await api.pickMedia( + arg_mediaSelectionOptions!, arg_generalOptions!); return [output]; }); } diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index 100a9b0490f8..8230dd7f130f 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,5 +1,6 @@ -## NEXT +## 2.2.0 +* Adds `getMedia` method. * Updates minimum supported SDK version to Flutter 3.3/Dart 2.18. ## 2.1.12 diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart index 9fe40da2557c..256fe3463b68 100644 --- a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart @@ -87,7 +87,8 @@ void main() { )); }); - testWidgets('Can select multiple files', (WidgetTester tester) async { + testWidgets('getMultiImage can select multiple files', + (WidgetTester tester) async { final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); final ImagePickerPluginTestOverrides overrides = @@ -117,6 +118,38 @@ void main() { expect(secondFile.length(), completion(secondTextFile.size)); }); + testWidgets('getMedia can select multiple files', + (WidgetTester tester) async { + final html.FileUploadInputElement mockInput = html.FileUploadInputElement(); + + final ImagePickerPluginTestOverrides overrides = + ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = + ((_) => [textFile, secondTextFile]); + + final ImagePickerPlugin plugin = ImagePickerPlugin(overrides: overrides); + + // Init the pick file dialog... + final Future> files = + plugin.getMedia(options: const MediaOptions(allowMultiple: true)); + + // Mock the browser behavior of selecting a file... + mockInput.dispatchEvent(html.Event('change')); + + // Now the file should be available + expect(files, completes); + + // And readable + expect((await files).first.readAsBytes(), completion(isNotEmpty)); + + // Peek into the second file... + final XFile secondFile = (await files).elementAt(1); + expect(secondFile.readAsBytes(), completion(isNotEmpty)); + expect(secondFile.name, secondTextFile.name); + expect(secondFile.length(), completion(secondTextFile.size)); + }); + // There's no good way of detecting when the user has "aborted" the selection. testWidgets('computeCaptureAttribute', (WidgetTester tester) async { diff --git a/packages/image_picker/image_picker_for_web/example/pubspec.yaml b/packages/image_picker/image_picker_for_web/example/pubspec.yaml index 9c431bd6e90d..433a1601834a 100644 --- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: sdk: flutter image_picker_for_web: path: ../ - image_picker_platform_interface: ^2.2.0 + image_picker_platform_interface: ^2.8.0 dev_dependencies: flutter_driver: diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index bb261f76f320..fb88c96a5942 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -8,6 +8,7 @@ import 'dart:html' as html; import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart' as mime; import 'src/image_resizer.dart'; @@ -166,7 +167,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { return files.first; } - /// Injects a file input, and returns a list of XFile that the user selected locally. + /// Injects a file input, and returns a list of XFile images that the user selected locally. @override Future> getMultiImage({ double? maxWidth, @@ -189,6 +190,30 @@ class ImagePickerPlugin extends ImagePickerPlatform { return Future.wait(resized); } + /// Injects a file input, and returns a list of XFile media that the user selected locally. + @override + Future> getMedia({ + required MediaOptions options, + }) async { + final List images = await getFiles( + accept: '$_kAcceptImageMimeType,$_kAcceptVideoMimeType', + multiple: options.allowMultiple, + ); + final Iterable> resized = images.map((XFile media) { + if (mime.lookupMimeType(media.path)?.startsWith('image/') ?? false) { + return _imageResizer.resizeImageIfNeeded( + media, + options.imageOptions.maxWidth, + options.imageOptions.maxHeight, + options.imageOptions.imageQuality, + ); + } + return Future.value(media); + }); + + return Future.wait(resized); + } + /// Injects a file input with the specified accept+capture attributes, and /// returns a list of XFile that the user selected locally. /// diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 06a7093f5962..a61a5b838c30 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_for_web description: Web platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_for_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 2.1.12 +version: 2.2.0 environment: sdk: ">=2.18.0 <4.0.0" @@ -21,7 +21,8 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - image_picker_platform_interface: ^2.2.0 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md index 1173ddf27b7b..78805ad58139 100644 --- a/packages/image_picker/image_picker_ios/CHANGELOG.md +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -1,4 +1,9 @@ +## 0.8.8 + +* Adds `getMedia` and `getMultipleMedia` methods. + ## 0.8.7+4 + * Fixes `BuildContext` handling in example. * Updates metadata unit test to work on iOS 16.2. diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m index ede62336a9c9..cc2262179ef8 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImagePickerPluginTests.m @@ -182,6 +182,32 @@ - (void)testPickMultiImageShouldUseUIImagePickerControllerOnPreiOS14 { [mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]); } +- (void)testPickMediaShouldUseUIImagePickerControllerOnPreiOS14 { + if (@available(iOS 14, *)) { + return; + } + + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id photoLibrary = OCMClassMock([PHPhotoLibrary class]); + OCMStub(ClassMethod([photoLibrary authorizationStatus])) + .andReturn(PHAuthorizationStatusAuthorized); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]]; + FLTMediaSelectionOptions *mediaSelectionOptions = + [FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + imageQuality:@(50) + requestFullMetadata:@YES + allowMultiple:@YES]; + + [plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + OCMVerify(times(1), + [mockUIImagePicker setSourceType:UIImagePickerControllerSourceTypePhotoLibrary]); +} + - (void)testPickImageWithoutFullMetadata { id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); id photoLibrary = OCMClassMock([PHPhotoLibrary class]); @@ -217,6 +243,28 @@ - (void)testPickMultiImageWithoutFullMetadata { OCMVerify(times(0), [photoLibrary authorizationStatus]); } +- (void)testPickMediaWithoutFullMetadata { + id mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + id photoLibrary = OCMClassMock([PHPhotoLibrary class]); + + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + [plugin setImagePickerControllerOverrides:@[ mockUIImagePicker ]]; + + FLTMediaSelectionOptions *mediaSelectionOptions = + [FLTMediaSelectionOptions makeWithMaxSize:[FLTMaxSize makeWithWidth:@(100) height:@(200)] + imageQuality:@(50) + requestFullMetadata:@YES + allowMultiple:@YES]; + + [plugin pickMediaWithMediaSelectionOptions:mediaSelectionOptions + + completion:^(NSArray *_Nullable result, + FlutterError *_Nullable error){ + }]; + + OCMVerify(times(0), [photoLibrary authorizationStatus]); +} + #pragma mark - Test camera devices, no op on simulators - (void)testPluginPickImageDeviceCancelClickMultipleTimes { @@ -298,6 +346,36 @@ - (void)testPluginMultiImagePathHasItem { [self waitForExpectationsWithTimeout:30 handler:nil]; } +- (void)testPluginMediaPathHasNoItem { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { + XCTAssertEqualObjects(result, @[]); + [resultExpectation fulfill]; + }]; + [plugin sendCallResultWithSavedPathList:@[]]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + +- (void)testPluginMediaPathHasItem { + FLTImagePickerPlugin *plugin = [[FLTImagePickerPlugin alloc] init]; + NSArray *pathList = @[ @"test" ]; + + XCTestExpectation *resultExpectation = [self expectationWithDescription:@"result"]; + + plugin.callContext = [[FLTImagePickerMethodCallContext alloc] + initWithResult:^(NSArray *_Nullable result, FlutterError *_Nullable error) { + XCTAssertEqualObjects(result, pathList); + [resultExpectation fulfill]; + }]; + [plugin sendCallResultWithSavedPathList:pathList]; + + [self waitForExpectationsWithTimeout:30 handler:nil]; +} + - (void)testSendsImageInvalidSourceError API_AVAILABLE(ios(14)) { id mockPickerViewController = OCMClassMock([PHPickerViewController class]); diff --git a/packages/image_picker/image_picker_ios/example/lib/main.dart b/packages/image_picker/image_picker_ios/example/lib/main.dart index 76076a5dbd65..0f42b58ad2dd 100755 --- a/packages/image_picker/image_picker_ios/example/lib/main.dart +++ b/packages/image_picker/image_picker_ios/example/lib/main.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -38,14 +39,14 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; - bool isVideo = false; + bool _isVideo = false; VideoPlayerController? _controller; VideoPlayerController? _toBeDisposed; @@ -60,18 +61,10 @@ class _MyHomePageState extends State { if (file != null && mounted) { await _disposeVideoController(); late VideoPlayerController controller; - if (kIsWeb) { - controller = VideoPlayerController.network(file.path); - } else { - controller = VideoPlayerController.file(File(file.path)); - } + + controller = VideoPlayerController.file(File(file.path)); _controller = controller; - // In web, most browsers won't honor a programmatic call to .play - // if the video has a sound track (and is not muted). - // Mute the video so it auto-plays in web! - // This is not needed if the call to .play is the result of user - // interaction (clicking on a "play" button, for example). - const double volume = kIsWeb ? 0.0 : 1.0; + const double volume = 1.0; await controller.setVolume(volume); await controller.initialize(); await controller.setLooping(true); @@ -80,13 +73,17 @@ class _MyHomePageState extends State { } } - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } if (context.mounted) { - if (isVideo) { + if (_isVideo) { final XFile? file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); @@ -94,18 +91,27 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { - final List pickedFileList = - await _picker.getMultiImageWithOptions( - options: MultiImagePickerOptions( - imageOptions: ImageOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), - ), - ); + final List pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); setState(() { - _imageFileList = pickedFileList; + _mediaFileList = pickedFileList; }); } catch (e) { setState(() { @@ -113,6 +119,31 @@ class _MyHomePageState extends State { }); } }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() => _pickImageError = e); + } + }); } else { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { @@ -186,22 +217,28 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { - // Why network for web? - // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform + final String? mime = lookupMimeType(_mediaFileList![index].path); return Semantics( label: 'image_picker_example_picked_image', - child: kIsWeb - ? Image.network(_imageFileList![index].path) - : Image.file(File(_imageFileList![index].path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -217,8 +254,19 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = kIsWeb ? 0.0 : 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { - if (isVideo) { + if (_isVideo) { return _previewVideo(); } else { return _previewImages(); @@ -240,8 +288,9 @@ class _MyHomePageState extends State { Semantics( label: 'image_picker_example_from_gallery', child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), onPressed: () { - isVideo = false; + _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'image0', @@ -253,7 +302,40 @@ class _MyHomePageState extends State { padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { - isVideo = false; + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; _onImageButtonPressed( ImageSource.gallery, context: context, @@ -269,7 +351,7 @@ class _MyHomePageState extends State { padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { - isVideo = false; + _isVideo = false; _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'image2', @@ -282,7 +364,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { - isVideo = true; + _isVideo = true; _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'video0', @@ -295,7 +377,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( backgroundColor: Colors.red, onPressed: () { - isVideo = true; + _isVideo = true; _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'video1', @@ -428,3 +510,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_ios/example/pubspec.yaml b/packages/image_picker/image_picker_ios/example/pubspec.yaml index d0bca043d1e4..9d0863537003 100755 --- a/packages/image_picker/image_picker_ios/example/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/example/pubspec.yaml @@ -16,7 +16,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - image_picker_platform_interface: ^2.6.1 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h index 0016765a0fe0..212f09236b0f 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.h @@ -16,7 +16,10 @@ NS_ASSUME_NONNULL_BEGIN + (nullable PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)); -// Save image with correct meta data and extention copied from the original asset. +// Saves video to temporary URL. Returns nil on failure; ++ (NSURL *)saveVideoFromURL:(NSURL *)videoURL; + +// Saves image with correct meta data and extention copied from the original asset. // maxWidth and maxHeight are used only for GIF images. + (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData image:(UIImage *)image diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m index bf712cdce39a..294bbc77947a 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPhotoAssetUtil.m @@ -20,6 +20,20 @@ + (PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(i return fetchResult.firstObject; } ++ (NSURL *)saveVideoFromURL:(NSURL *)videoURL { + if (![[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) { + return nil; + } + NSString *fileName = [videoURL lastPathComponent]; + NSURL *destination = [NSURL fileURLWithPath:[self temporaryFilePath:fileName]]; + NSError *error; + [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; + if (error) { + return nil; + } + return destination; +} + + (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData image:(UIImage *)image maxWidth:(NSNumber *)maxWidth diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m index 5aadecdf9482..c812e35186c6 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin.m @@ -109,7 +109,13 @@ - (void)launchPHPickerWithContext:(nonnull FLTImagePickerMethodCallContext *)con PHPickerConfiguration *config = [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; config.selectionLimit = context.maxImageCount; - config.filter = [PHPickerFilter imagesFilter]; + if (context.includeVideo) { + config.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[ + [PHPickerFilter imagesFilter], [PHPickerFilter videosFilter] + ]]; + } else { + config.filter = [PHPickerFilter imagesFilter]; + } _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; _pickerViewController.delegate = self; @@ -128,7 +134,12 @@ - (void)launchUIImagePickerWithSource:(nonnull FLTSourceSpecification *)source UIImagePickerController *imagePickerController = [self createImagePickerController]; imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; imagePickerController.delegate = self; - imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + if (context.includeVideo) { + imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage, (NSString *)kUTTypeMovie ]; + + } else { + imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + } self.callContext = context; switch (source.type) { @@ -206,6 +217,29 @@ - (void)pickMultiImageWithMaxSize:(nonnull FLTMaxSize *)maxSize } } +- (void)pickMediaWithMediaSelectionOptions:(nonnull FLTMediaSelectionOptions *)mediaSelectionOptions + completion:(nonnull void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion { + FLTImagePickerMethodCallContext *context = + [[FLTImagePickerMethodCallContext alloc] initWithResult:completion]; + context.maxSize = [mediaSelectionOptions maxSize]; + context.imageQuality = [mediaSelectionOptions imageQuality]; + context.requestFullMetadata = [mediaSelectionOptions requestFullMetadata]; + context.includeVideo = YES; + if (![[mediaSelectionOptions allowMultiple] boolValue]) { + context.maxImageCount = 1; + } + + if (@available(iOS 14, *)) { + [self launchPHPickerWithContext:context]; + } else { + // Camera is ignored for gallery mode, so the value here is arbitrary. + [self launchUIImagePickerWithSource:[FLTSourceSpecification makeWithType:FLTSourceTypeGallery + camera:FLTSourceCameraRear] + context:context]; + } +} + - (void)pickVideoWithSource:(nonnull FLTSourceSpecification *)source maxDuration:(nullable NSNumber *)maxDurationSeconds completion: @@ -538,25 +572,16 @@ - (void)imagePickerController:(UIImagePickerController *)picker } if (videoURL != nil) { if (@available(iOS 13.0, *)) { - NSString *fileName = [videoURL lastPathComponent]; - NSURL *destination = - [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:fileName]]; - - if ([[NSFileManager defaultManager] isReadableFileAtPath:[videoURL path]]) { - NSError *error; - if (![[videoURL path] isEqualToString:[destination path]]) { - [[NSFileManager defaultManager] copyItemAtURL:videoURL toURL:destination error:&error]; - - if (error) { - [self sendCallResultWithError:[FlutterError - errorWithCode:@"flutter_image_picker_copy_video_error" - message:@"Could not cache the video file." - details:nil]]; - return; - } - } - videoURL = destination; + NSURL *destination = [FLTImagePickerPhotoAssetUtil saveVideoFromURL:videoURL]; + if (destination == nil) { + [self sendCallResultWithError:[FlutterError + errorWithCode:@"flutter_image_picker_copy_video_error" + message:@"Could not cache the video file." + details:nil]]; + return; } + + videoURL = destination; } [self sendCallResultWithSavedPathList:@[ videoURL.path ]]; } else { diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h index f84921160a31..99d3ef6e195b 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTImagePickerPlugin_Test.h @@ -11,7 +11,7 @@ NS_ASSUME_NONNULL_BEGIN /** - * The return hander used for all method calls, which internally adapts the provided result list + * The return handler used for all method calls, which internally adapts the provided result list * to return either a list or a single element depending on the original call. */ typedef void (^FlutterResultAdapter)(NSArray *_Nullable, FlutterError *_Nullable); @@ -49,6 +49,9 @@ typedef void (^FlutterResultAdapter)(NSArray *_Nullable, FlutterErro /** Whether the image should be picked with full metadata (requires gallery permissions) */ @property(nonatomic, assign) BOOL requestFullMetadata; +/** Whether the picker should include videos in the list*/ +@property(nonatomic, assign) BOOL includeVideo; + @end #pragma mark - diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m index 80e03ddd6578..3476721ae615 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m @@ -107,9 +107,15 @@ - (void)start { [self completeOperationWithPath:nil error:flutterError]; } }]; + } else if ([self.result.itemProvider + // This supports uniform types that conform to UTTypeMovie. + // This includes kUTTypeVideo, kUTTypeMPEG4, public.3gpp, kUTTypeMPEG, + // public.3gpp2, public.avi, kUTTypeQuickTimeMovie. + hasItemConformingToTypeIdentifier:UTTypeMovie.identifier]) { + [self processVideo]; } else { FlutterError *flutterError = [FlutterError errorWithCode:@"invalid_source" - message:@"Invalid image source." + message:@"Invalid media source." details:nil]; [self completeOperationWithPath:nil error:flutterError]; } @@ -184,4 +190,41 @@ - (void)processImage:(NSData *)pickerImageData API_AVAILABLE(ios(14)) { } } +/** + * Processes the video. + */ +- (void)processVideo API_AVAILABLE(ios(14)) { + NSString *typeIdentifier = self.result.itemProvider.registeredTypeIdentifiers.firstObject; + [self.result.itemProvider + loadFileRepresentationForTypeIdentifier:typeIdentifier + completionHandler:^(NSURL *_Nullable videoURL, + NSError *_Nullable error) { + if (error != nil) { + FlutterError *flutterError = + [FlutterError errorWithCode:@"invalid_image" + message:error.localizedDescription + details:error.domain]; + [self completeOperationWithPath:nil error:flutterError]; + return; + } + + NSURL *destination = + [FLTImagePickerPhotoAssetUtil saveVideoFromURL:videoURL]; + if (destination == nil) { + [self + completeOperationWithPath:nil + error:[FlutterError + errorWithCode: + @"flutter_image_picker_copy_" + @"video_error" + message:@"Could not cache " + @"the video file." + details:nil]]; + return; + } + + [self completeOperationWithPath:[destination path] error:nil]; + }]; +} + @end diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h index cdde03d50550..4e2c4b28c1f6 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.h @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -24,6 +24,7 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) { }; @class FLTMaxSize; +@class FLTMediaSelectionOptions; @class FLTSourceSpecification; @interface FLTMaxSize : NSObject @@ -32,6 +33,19 @@ typedef NS_ENUM(NSUInteger, FLTSourceType) { @property(nonatomic, strong, nullable) NSNumber *height; @end +@interface FLTMediaSelectionOptions : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithMaxSize:(FLTMaxSize *)maxSize + imageQuality:(nullable NSNumber *)imageQuality + requestFullMetadata:(NSNumber *)requestFullMetadata + allowMultiple:(NSNumber *)allowMultiple; +@property(nonatomic, strong) FLTMaxSize *maxSize; +@property(nonatomic, strong, nullable) NSNumber *imageQuality; +@property(nonatomic, strong) NSNumber *requestFullMetadata; +@property(nonatomic, strong) NSNumber *allowMultiple; +@end + @interface FLTSourceSpecification : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -57,6 +71,10 @@ NSObject *FLTImagePickerApiGetCodec(void); - (void)pickVideoWithSource:(FLTSourceSpecification *)source maxDuration:(nullable NSNumber *)maxDurationSeconds completion:(void (^)(NSString *_Nullable, FlutterError *_Nullable))completion; +/// Selects images and videos and returns their paths. +- (void)pickMediaWithMediaSelectionOptions:(FLTMediaSelectionOptions *)mediaSelectionOptions + completion:(void (^)(NSArray *_Nullable, + FlutterError *_Nullable))completion; @end extern void FLTImagePickerApiSetup(id binaryMessenger, diff --git a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m index a1d863639c44..2a24f8367037 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/messages.g.m @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.g.h" @@ -30,6 +30,12 @@ + (nullable FLTMaxSize *)nullableFromList:(NSArray *)list; - (NSArray *)toList; @end +@interface FLTMediaSelectionOptions () ++ (FLTMediaSelectionOptions *)fromList:(NSArray *)list; ++ (nullable FLTMediaSelectionOptions *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + @interface FLTSourceSpecification () + (FLTSourceSpecification *)fromList:(NSArray *)list; + (nullable FLTSourceSpecification *)nullableFromList:(NSArray *)list; @@ -60,6 +66,42 @@ - (NSArray *)toList { } @end +@implementation FLTMediaSelectionOptions ++ (instancetype)makeWithMaxSize:(FLTMaxSize *)maxSize + imageQuality:(nullable NSNumber *)imageQuality + requestFullMetadata:(NSNumber *)requestFullMetadata + allowMultiple:(NSNumber *)allowMultiple { + FLTMediaSelectionOptions *pigeonResult = [[FLTMediaSelectionOptions alloc] init]; + pigeonResult.maxSize = maxSize; + pigeonResult.imageQuality = imageQuality; + pigeonResult.requestFullMetadata = requestFullMetadata; + pigeonResult.allowMultiple = allowMultiple; + return pigeonResult; +} ++ (FLTMediaSelectionOptions *)fromList:(NSArray *)list { + FLTMediaSelectionOptions *pigeonResult = [[FLTMediaSelectionOptions alloc] init]; + pigeonResult.maxSize = [FLTMaxSize nullableFromList:(GetNullableObjectAtIndex(list, 0))]; + NSAssert(pigeonResult.maxSize != nil, @""); + pigeonResult.imageQuality = GetNullableObjectAtIndex(list, 1); + pigeonResult.requestFullMetadata = GetNullableObjectAtIndex(list, 2); + NSAssert(pigeonResult.requestFullMetadata != nil, @""); + pigeonResult.allowMultiple = GetNullableObjectAtIndex(list, 3); + NSAssert(pigeonResult.allowMultiple != nil, @""); + return pigeonResult; +} ++ (nullable FLTMediaSelectionOptions *)nullableFromList:(NSArray *)list { + return (list) ? [FLTMediaSelectionOptions fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + (self.maxSize ? [self.maxSize toList] : [NSNull null]), + (self.imageQuality ?: [NSNull null]), + (self.requestFullMetadata ?: [NSNull null]), + (self.allowMultiple ?: [NSNull null]), + ]; +} +@end + @implementation FLTSourceSpecification + (instancetype)makeWithType:(FLTSourceType)type camera:(FLTSourceCamera)camera { FLTSourceSpecification *pigeonResult = [[FLTSourceSpecification alloc] init]; @@ -92,6 +134,8 @@ - (nullable id)readValueOfType:(UInt8)type { case 128: return [FLTMaxSize fromList:[self readValue]]; case 129: + return [FLTMediaSelectionOptions fromList:[self readValue]]; + case 130: return [FLTSourceSpecification fromList:[self readValue]]; default: return [super readValueOfType:type]; @@ -106,9 +150,12 @@ - (void)writeValue:(id)value { if ([value isKindOfClass:[FLTMaxSize class]]) { [self writeByte:128]; [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FLTSourceSpecification class]]) { + } else if ([value isKindOfClass:[FLTMediaSelectionOptions class]]) { [self writeByte:129]; [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FLTSourceSpecification class]]) { + [self writeByte:130]; + [self writeValue:[value toList]]; } else { [super writeValue:value]; } @@ -220,4 +267,28 @@ void FLTImagePickerApiSetup(id binaryMessenger, [channel setMessageHandler:nil]; } } + /// Selects images and videos and returns their paths. + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.ImagePickerApi.pickMedia" + binaryMessenger:binaryMessenger + codec:FLTImagePickerApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector(pickMediaWithMediaSelectionOptions:completion:)], + @"FLTImagePickerApi api (%@) doesn't respond to " + @"@selector(pickMediaWithMediaSelectionOptions:completion:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + FLTMediaSelectionOptions *arg_mediaSelectionOptions = GetNullableObjectAtIndex(args, 0); + [api pickMediaWithMediaSelectionOptions:arg_mediaSelectionOptions + completion:^(NSArray *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; + }]; + } else { + [channel setMessageHandler:nil]; + } + } } diff --git a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart index 3f76784ff07c..02105f95e5a1 100644 --- a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart +++ b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart @@ -175,6 +175,51 @@ class ImagePickerIOS extends ImagePickerPlatform { ); } + @override + Future> getMedia({ + required MediaOptions options, + }) async { + final MediaSelectionOptions mediaSelectionOptions = + _mediaOptionsToMediaSelectionOptions(options); + + return (await _hostApi.pickMedia(mediaSelectionOptions)) + .map((String? path) => XFile(path!)) + .toList(); + } + + MaxSize _imageOptionsToMaxSizeWithValidation(ImageOptions imageOptions) { + final double? maxHeight = imageOptions.maxHeight; + final double? maxWidth = imageOptions.maxWidth; + final int? imageQuality = imageOptions.imageQuality; + + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + + return MaxSize(width: maxWidth, height: maxHeight); + } + + MediaSelectionOptions _mediaOptionsToMediaSelectionOptions( + MediaOptions mediaOptions) { + final MaxSize maxSize = + _imageOptionsToMaxSizeWithValidation(mediaOptions.imageOptions); + return MediaSelectionOptions( + maxSize: maxSize, + imageQuality: mediaOptions.imageOptions.imageQuality, + requestFullMetadata: mediaOptions.imageOptions.requestFullMetadata, + allowMultiple: mediaOptions.allowMultiple, + ); + } + @override Future pickVideo({ required ImageSource source, diff --git a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart index 87596b78ebf6..91dde827a60e 100644 --- a/packages/image_picker/image_picker_ios/lib/src/messages.g.dart +++ b/packages/image_picker/image_picker_ios/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import @@ -47,6 +47,42 @@ class MaxSize { } } +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.maxSize, + this.imageQuality, + required this.requestFullMetadata, + required this.allowMultiple, + }); + + MaxSize maxSize; + + int? imageQuality; + + bool requestFullMetadata; + + bool allowMultiple; + + Object encode() { + return [ + maxSize.encode(), + imageQuality, + requestFullMetadata, + allowMultiple, + ]; + } + + static MediaSelectionOptions decode(Object result) { + result as List; + return MediaSelectionOptions( + maxSize: MaxSize.decode(result[0]! as List), + imageQuality: result[1] as int?, + requestFullMetadata: result[2]! as bool, + allowMultiple: result[3]! as bool, + ); + } +} + class SourceSpecification { SourceSpecification({ required this.type, @@ -80,9 +116,12 @@ class _ImagePickerApiCodec extends StandardMessageCodec { if (value is MaxSize) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is SourceSpecification) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -94,6 +133,8 @@ class _ImagePickerApiCodec extends StandardMessageCodec { case 128: return MaxSize.decode(readValue(buffer)!); case 129: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 130: return SourceSpecification.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -184,4 +225,33 @@ class ImagePickerApi { return (replyList[0] as String?); } } + + /// Selects images and videos and returns their paths. + Future> pickMedia( + MediaSelectionOptions arg_mediaSelectionOptions) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_mediaSelectionOptions]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as List?)!.cast(); + } + } } diff --git a/packages/image_picker/image_picker_ios/pigeons/messages.dart b/packages/image_picker/image_picker_ios/pigeons/messages.dart index d04841b0fde9..fb69a6d13349 100644 --- a/packages/image_picker/image_picker_ios/pigeons/messages.dart +++ b/packages/image_picker/image_picker_ios/pigeons/messages.dart @@ -20,6 +20,20 @@ class MaxSize { double? height; } +class MediaSelectionOptions { + MediaSelectionOptions({ + required this.maxSize, + this.imageQuality, + required this.requestFullMetadata, + required this.allowMultiple, + }); + + MaxSize maxSize; + int? imageQuality; + bool requestFullMetadata; + bool allowMultiple; +} + // Corresponds to `CameraDevice` from the platform interface package. enum SourceCamera { rear, front } @@ -45,4 +59,9 @@ abstract class ImagePickerApi { @async @ObjCSelector('pickVideoWithSource:maxDuration:') String? pickVideo(SourceSpecification source, int? maxDurationSeconds); + + /// Selects images and videos and returns their paths. + @async + @ObjCSelector('pickMediaWithMediaSelectionOptions:') + List pickMedia(MediaSelectionOptions mediaSelectionOptions); } diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml index 90d6dff73d60..a9246891458c 100755 --- a/packages/image_picker/image_picker_ios/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_ios description: iOS implementation of the image_picker plugin. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.7+4 +version: 0.8.8 environment: sdk: ">=2.18.0 <4.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - image_picker_platform_interface: ^2.6.1 + image_picker_platform_interface: ^2.8.0 dev_dependencies: flutter_test: diff --git a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart index 2c9d52509f26..da74e31f0a33 100644 --- a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart +++ b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart @@ -71,6 +71,19 @@ class _ApiLogger implements TestHostImagePickerApi { return returnValue as List?; } + @override + Future> pickMedia( + MediaSelectionOptions mediaSelectionOptions) async { + calls.add(_LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': mediaSelectionOptions.maxSize.width, + 'maxHeight': mediaSelectionOptions.maxSize.height, + 'imageQuality': mediaSelectionOptions.imageQuality, + 'requestFullMetadata': mediaSelectionOptions.requestFullMetadata, + 'allowMultiple': mediaSelectionOptions.allowMultiple, + })); + return returnValue as List; + } + @override Future pickVideo( SourceSpecification source, int? maxDurationSeconds) async { @@ -878,6 +891,227 @@ void main() { }); }); + group('#getMedia', () { + test('calls the method correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMedia(options: const MediaOptions(allowMultiple: true)); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxHeight: 10.0, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + maxHeight: 20.0, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + imageQuality: 70, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxHeight: 10.0, + imageQuality: 70, + ), + )); + await picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ), + )); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'requestFullMetadata': true, + 'allowMultiple': true + }), + ], + ); + }); + + test('passes request metadata argument correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMedia( + options: const MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions(requestFullMetadata: false), + )); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': false, + 'allowMultiple': true + }), + ], + ); + }); + + test('passes allowMultiple argument correctly', () async { + log.returnValue = ['0', '1']; + await picker.getMedia( + options: const MediaOptions( + allowMultiple: false, + )); + + expect( + log.calls, + <_LoggedMethodCall>[ + const _LoggedMethodCall('pickMedia', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'requestFullMetadata': true, + 'allowMultiple': false + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate(maxWidth: -1.0), + )), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate(maxHeight: -1.0), + )), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + log.returnValue = ['0', '1']; + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate(imageQuality: -1), + )), + throwsArgumentError, + ); + + expect( + () => picker.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate(imageQuality: 101), + )), + throwsArgumentError, + ); + }); + + test('handles a empty path response gracefully', () async { + log.returnValue = []; + + expect( + await picker.getMedia( + options: const MediaOptions(allowMultiple: true)), + []); + }); + }); + group('#getVideo', () { test('passes the image source argument correctly', () async { await picker.getVideo(source: ImageSource.camera); diff --git a/packages/image_picker/image_picker_ios/test/test_api.g.dart b/packages/image_picker/image_picker_ios/test/test_api.g.dart index 4ac619590f93..6da0400b1a07 100644 --- a/packages/image_picker/image_picker_ios/test/test_api.g.dart +++ b/packages/image_picker/image_picker_ios/test/test_api.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import // ignore_for_file: avoid_relative_lib_imports @@ -20,9 +20,12 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { if (value is MaxSize) { buffer.putUint8(128); writeValue(buffer, value.encode()); - } else if (value is SourceSpecification) { + } else if (value is MediaSelectionOptions) { buffer.putUint8(129); writeValue(buffer, value.encode()); + } else if (value is SourceSpecification) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -34,6 +37,8 @@ class _TestHostImagePickerApiCodec extends StandardMessageCodec { case 128: return MaxSize.decode(readValue(buffer)!); case 129: + return MediaSelectionOptions.decode(readValue(buffer)!); + case 130: return SourceSpecification.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -55,6 +60,9 @@ abstract class TestHostImagePickerApi { Future pickVideo( SourceSpecification source, int? maxDurationSeconds); + /// Selects images and videos and returns their paths. + Future> pickMedia(MediaSelectionOptions mediaSelectionOptions); + static void setup(TestHostImagePickerApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -140,5 +148,29 @@ abstract class TestHostImagePickerApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImagePickerApi.pickMedia', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null.'); + final List args = (message as List?)!; + final MediaSelectionOptions? arg_mediaSelectionOptions = + (args[0] as MediaSelectionOptions?); + assert(arg_mediaSelectionOptions != null, + 'Argument for dev.flutter.pigeon.ImagePickerApi.pickMedia was null, expected non-null MediaSelectionOptions.'); + final List output = + await api.pickMedia(arg_mediaSelectionOptions!); + return [output]; + }); + } + } } } diff --git a/packages/image_picker/image_picker_linux/CHANGELOG.md b/packages/image_picker/image_picker_linux/CHANGELOG.md index d3bfbf901bbd..9f14cc71ced6 100644 --- a/packages/image_picker/image_picker_linux/CHANGELOG.md +++ b/packages/image_picker/image_picker_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.1 + +* Adds `getMedia` method. + ## 0.2.0 * Implements initial Linux support. diff --git a/packages/image_picker/image_picker_linux/example/lib/main.dart b/packages/image_picker/image_picker_linux/example/lib/main.dart index 9e22c716a2e0..8f4887095c13 100644 --- a/packages/image_picker/image_picker_linux/example/lib/main.dart +++ b/packages/image_picker/image_picker_linux/example/lib/main.dart @@ -9,6 +9,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; // This must be called from within a setState() callback void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -70,52 +71,12 @@ class _MyHomePageState extends State { } } - Future _handleMultiImagePicked(BuildContext context) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final List? pickedFileList = await _picker.getMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); - setState(() { - _imageFileList = pickedFileList; - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _handleSingleImagePicked( - BuildContext context, ImageSource source) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final XFile? pickedFile = await _picker.getImageFromSource( - source: source, - options: ImagePickerOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), - ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } @@ -125,9 +86,83 @@ class _MyHomePageState extends State { source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else if (isMultiImage) { - await _handleMultiImagePicked(context); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() => _pickImageError = e); + } + }); } else { - await _handleSingleImagePicked(context, source); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); } } } @@ -180,18 +215,28 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); return Semantics( label: 'image_picker_example_picked_image', - child: Image.file(File(_imageFileList![index].path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -207,6 +252,17 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { if (_isVideo) { return _previewVideo(); @@ -230,6 +286,7 @@ class _MyHomePageState extends State { Semantics( label: 'image_picker_example_from_gallery', child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), onPressed: () { _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); @@ -239,6 +296,39 @@ class _MyHomePageState extends State { child: const Icon(Icons.photo), ), ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( @@ -420,3 +510,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_linux/example/pubspec.yaml b/packages/image_picker/image_picker_linux/example/pubspec.yaml index 54beb765641c..76e8f25ac106 100644 --- a/packages/image_picker/image_picker_linux/example/pubspec.yaml +++ b/packages/image_picker/image_picker_linux/example/pubspec.yaml @@ -17,7 +17,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: .. - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart b/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart index f932a0211721..72596ea931f2 100644 --- a/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart +++ b/packages/image_picker/image_picker_linux/lib/image_picker_linux.dart @@ -154,4 +154,27 @@ class ImagePickerLinux extends CameraDelegatingImagePickerPlatform { .openFiles(acceptedTypeGroups: [typeGroup]); return files; } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently + // supported. If any of these arguments are supplied, they will be silently + // ignored. + @override + Future> getMedia({required MediaOptions options}) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images and videos', extensions: ['image/*', 'video/*']); + + List files; + + if (options.allowMultiple) { + files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + } else { + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + files = [ + if (file != null) file, + ]; + } + return files; + } } diff --git a/packages/image_picker/image_picker_linux/pubspec.yaml b/packages/image_picker/image_picker_linux/pubspec.yaml index dcfd6758ad79..9698991e336e 100644 --- a/packages/image_picker/image_picker_linux/pubspec.yaml +++ b/packages/image_picker/image_picker_linux/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_linux description: Linux platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.2.0 +version: 0.2.1 environment: sdk: ">=2.18.0 <4.0.0" @@ -20,7 +20,7 @@ dependencies: file_selector_platform_interface: ^2.2.0 flutter: sdk: flutter - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart index 32c3d4509142..004bfcc4dc85 100644 --- a/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart +++ b/packages/image_picker/image_picker_linux/test/image_picker_linux_test.dart @@ -125,6 +125,38 @@ void main() { plugin.getVideo(source: ImageSource.camera), throwsStateError); }); }); + + group('media', () { + test('getMedia passes the accepted type groups correctly', () async { + await plugin.getMedia(options: const MediaOptions(allowMultiple: true)); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, + ['image/*', 'video/*']); + }); + + test('multiple media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ), + []); + }); + + test('single media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: false, + ), + ), + []); + }); + }); } class FakeCameraDelegate extends ImagePickerCameraDelegate { diff --git a/packages/image_picker/image_picker_macos/CHANGELOG.md b/packages/image_picker/image_picker_macos/CHANGELOG.md index 94ce98bd3ab1..bd79a8674cd8 100644 --- a/packages/image_picker/image_picker_macos/CHANGELOG.md +++ b/packages/image_picker/image_picker_macos/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.1 + +* Adds `getMedia` method. + ## 0.2.0 * Implements initial macOS support. diff --git a/packages/image_picker/image_picker_macos/example/lib/main.dart b/packages/image_picker/image_picker_macos/example/lib/main.dart index 9e22c716a2e0..8f4887095c13 100644 --- a/packages/image_picker/image_picker_macos/example/lib/main.dart +++ b/packages/image_picker/image_picker_macos/example/lib/main.dart @@ -9,6 +9,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; // This must be called from within a setState() callback void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -70,52 +71,12 @@ class _MyHomePageState extends State { } } - Future _handleMultiImagePicked(BuildContext context) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final List? pickedFileList = await _picker.getMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); - setState(() { - _imageFileList = pickedFileList; - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _handleSingleImagePicked( - BuildContext context, ImageSource source) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final XFile? pickedFile = await _picker.getImageFromSource( - source: source, - options: ImagePickerOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), - ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } @@ -125,9 +86,83 @@ class _MyHomePageState extends State { source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else if (isMultiImage) { - await _handleMultiImagePicked(context); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() => _pickImageError = e); + } + }); } else { - await _handleSingleImagePicked(context, source); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); } } } @@ -180,18 +215,28 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); return Semantics( label: 'image_picker_example_picked_image', - child: Image.file(File(_imageFileList![index].path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -207,6 +252,17 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { if (_isVideo) { return _previewVideo(); @@ -230,6 +286,7 @@ class _MyHomePageState extends State { Semantics( label: 'image_picker_example_from_gallery', child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), onPressed: () { _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); @@ -239,6 +296,39 @@ class _MyHomePageState extends State { child: const Icon(Icons.photo), ), ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( @@ -420,3 +510,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_macos/example/pubspec.yaml b/packages/image_picker/image_picker_macos/example/pubspec.yaml index e76c49286ef6..785a2afb227b 100644 --- a/packages/image_picker/image_picker_macos/example/pubspec.yaml +++ b/packages/image_picker/image_picker_macos/example/pubspec.yaml @@ -17,7 +17,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: .. - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart index 7a7e92737b03..9e9447a5710c 100644 --- a/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart +++ b/packages/image_picker/image_picker_macos/lib/image_picker_macos.dart @@ -159,4 +159,28 @@ class ImagePickerMacOS extends CameraDelegatingImagePickerPlatform { .openFiles(acceptedTypeGroups: [typeGroup]); return files; } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not currently + // supported. If any of these arguments are supplied, they will be silently + // ignored. + @override + Future> getMedia({required MediaOptions options}) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images and videos', + extensions: ['public.image', 'public.movie']); + + List files; + + if (options.allowMultiple) { + files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + } else { + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + files = [ + if (file != null) file, + ]; + } + return files; + } } diff --git a/packages/image_picker/image_picker_macos/pubspec.yaml b/packages/image_picker/image_picker_macos/pubspec.yaml index ef97bd4bc257..9ace885e666f 100644 --- a/packages/image_picker/image_picker_macos/pubspec.yaml +++ b/packages/image_picker/image_picker_macos/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_macos description: macOS platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.2.0 +version: 0.2.1 environment: sdk: ">=2.18.0 <4.0.0" @@ -20,7 +20,7 @@ dependencies: file_selector_platform_interface: ^2.3.0 flutter: sdk: flutter - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart index f2b45cf33db9..7e94161d4a40 100644 --- a/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart +++ b/packages/image_picker/image_picker_macos/test/image_picker_macos_test.dart @@ -131,6 +131,38 @@ void main() { plugin.getVideo(source: ImageSource.camera), throwsStateError); }); }); + + group('media', () { + test('getMedia passes the accepted type groups correctly', () async { + await plugin.getMedia(options: const MediaOptions(allowMultiple: true)); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, + ['public.image', 'public.movie']); + }); + + test('multiple media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ), + []); + }); + + test('single media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: false, + ), + ), + []); + }); + }); } class FakeCameraDelegate extends ImagePickerCameraDelegate { diff --git a/packages/image_picker/image_picker_windows/CHANGELOG.md b/packages/image_picker/image_picker_windows/CHANGELOG.md index 2159d8701228..bd881d1fc3b5 100644 --- a/packages/image_picker/image_picker_windows/CHANGELOG.md +++ b/packages/image_picker/image_picker_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.1 + +* Adds `getMedia` method. + ## 0.2.0 * Updates minimum Flutter version to 3.3. diff --git a/packages/image_picker/image_picker_windows/example/lib/main.dart b/packages/image_picker/image_picker_windows/example/lib/main.dart index 9e22c716a2e0..8f4887095c13 100644 --- a/packages/image_picker/image_picker_windows/example/lib/main.dart +++ b/packages/image_picker/image_picker_windows/example/lib/main.dart @@ -9,6 +9,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -37,11 +38,11 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; // This must be called from within a setState() callback void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -70,52 +71,12 @@ class _MyHomePageState extends State { } } - Future _handleMultiImagePicked(BuildContext context) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final List? pickedFileList = await _picker.getMultiImage( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ); - setState(() { - _imageFileList = pickedFileList; - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _handleSingleImagePicked( - BuildContext context, ImageSource source) async { - await _displayPickImageDialog(context, - (double? maxWidth, double? maxHeight, int? quality) async { - try { - final XFile? pickedFile = await _picker.getImageFromSource( - source: source, - options: ImagePickerOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality, - ), - ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); - } catch (e) { - setState(() { - _pickImageError = e; - }); - } - }); - } - - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } @@ -125,9 +86,83 @@ class _MyHomePageState extends State { source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else if (isMultiImage) { - await _handleMultiImagePicked(context); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = isMedia + ? await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + ) + : await _picker.getMultiImageWithOptions( + options: MultiImagePickerOptions( + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ), + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = _firstOrNull(await _picker.getMedia( + options: MediaOptions( + allowMultiple: isMultiImage, + imageOptions: ImageOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + )), + )); + + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } + } catch (e) { + setState(() => _pickImageError = e); + } + }); } else { - await _handleSingleImagePicked(context, source); + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final XFile? pickedFile = await _picker.getImageFromSource( + source: source, + options: ImagePickerOptions( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ), + ); + setState(() { + _setImageFileListFromFile(pickedFile); + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); } } } @@ -180,18 +215,28 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); return Semantics( label: 'image_picker_example_picked_image', - child: Image.file(File(_imageFileList![index].path)), + child: mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -207,6 +252,17 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { if (_isVideo) { return _previewVideo(); @@ -230,6 +286,7 @@ class _MyHomePageState extends State { Semantics( label: 'image_picker_example_from_gallery', child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), onPressed: () { _isVideo = false; _onImageButtonPressed(ImageSource.gallery, context: context); @@ -239,6 +296,39 @@ class _MyHomePageState extends State { child: const Icon(Icons.photo), ), ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + _isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( @@ -420,3 +510,7 @@ class AspectRatioVideoState extends State { } } } + +T? _firstOrNull(List list) { + return list.isEmpty ? null : list.first; +} diff --git a/packages/image_picker/image_picker_windows/example/pubspec.yaml b/packages/image_picker/image_picker_windows/example/pubspec.yaml index a645670f379d..6515d507696a 100644 --- a/packages/image_picker/image_picker_windows/example/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/example/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 image_picker_windows: # When depending on this package from a real application you should use: # image_picker_windows: ^x.y.z @@ -18,6 +18,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: .. + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart index ba7ff4d6e70f..e9e414628c93 100644 --- a/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart +++ b/packages/image_picker/image_picker_windows/lib/image_picker_windows.dart @@ -183,4 +183,28 @@ class ImagePickerWindows extends CameraDelegatingImagePickerPlatform { .openFiles(acceptedTypeGroups: [typeGroup]); return files; } + + // `maxWidth`, `maxHeight`, and `imageQuality` arguments are not + // supported on Windows. If any of these arguments is supplied, + // they will be silently ignored by the Windows version of the plugin. + @override + Future> getMedia({required MediaOptions options}) async { + const XTypeGroup typeGroup = XTypeGroup( + label: 'images and videos', + extensions: [...imageFormats, ...videoFormats]); + + List files; + + if (options.allowMultiple) { + files = await fileSelector + .openFiles(acceptedTypeGroups: [typeGroup]); + } else { + final XFile? file = await fileSelector + .openFile(acceptedTypeGroups: [typeGroup]); + files = [ + if (file != null) file, + ]; + } + return files; + } } diff --git a/packages/image_picker/image_picker_windows/pubspec.yaml b/packages/image_picker/image_picker_windows/pubspec.yaml index 2ca2fc555739..e16ecbda993b 100644 --- a/packages/image_picker/image_picker_windows/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker_windows description: Windows platform implementation of image_picker repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.2.0 +version: 0.2.1 environment: sdk: ">=2.18.0 <4.0.0" @@ -20,7 +20,7 @@ dependencies: file_selector_windows: ^0.9.0 flutter: sdk: flutter - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 dev_dependencies: build_runner: ^2.1.5 diff --git a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart index d680d782f6cb..6da0873af5b4 100644 --- a/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart +++ b/packages/image_picker/image_picker_windows/test/image_picker_windows_test.dart @@ -128,6 +128,41 @@ void main() { plugin.getVideo(source: ImageSource.camera), throwsStateError); }); }); + + group('media', () { + test('getMedia passes the accepted type groups correctly', () async { + await plugin.getMedia(options: const MediaOptions(allowMultiple: true)); + + final VerificationResult result = verify( + mockFileSelectorPlatform.openFiles( + acceptedTypeGroups: captureAnyNamed('acceptedTypeGroups'))); + expect(capturedTypeGroups(result)[0].extensions, [ + ...ImagePickerWindows.imageFormats, + ...ImagePickerWindows.videoFormats + ]); + }); + + test('multiple media handles an empty path response gracefully', + () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: true, + ), + ), + []); + }); + + test('single media handles an empty path response gracefully', () async { + expect( + await plugin.getMedia( + options: const MediaOptions( + allowMultiple: false, + ), + ), + []); + }); + }); }); } diff --git a/script/configs/allowed_unpinned_deps.yaml b/script/configs/allowed_unpinned_deps.yaml index 1cf35c8881e2..d027396f93dd 100644 --- a/script/configs/allowed_unpinned_deps.yaml +++ b/script/configs/allowed_unpinned_deps.yaml @@ -48,6 +48,7 @@ - logging - markdown - meta +- mime - path - shelf - shelf_static From c0cba0b5cf49e209942ec59683d6daec498c4fb7 Mon Sep 17 00:00:00 2001 From: Tarrin Neal Date: Wed, 14 Jun 2023 22:06:01 -0700 Subject: [PATCH 33/53] [image_picker] add getMedia method (#3892) Adds `getMedia` and `getMultipleMedia` methods to all platforms. fixes https://github.com/flutter/flutter/issues/89159 --- .../image_picker/image_picker/CHANGELOG.md | 4 + packages/image_picker/image_picker/README.md | 6 +- .../image_picker/example/lib/main.dart | 121 +++++- .../example/lib/readme_excerpts.dart | 8 +- .../image_picker/example/pubspec.yaml | 3 +- .../example/test/readme_excerpts_test.dart | 7 + .../image_picker/lib/image_picker.dart | 163 ++++++-- .../image_picker/image_picker/pubspec.yaml | 4 +- .../image_picker/test/image_picker_test.dart | 367 +++++++++++++++++- .../test/image_picker_test.mocks.dart | 10 + 10 files changed, 627 insertions(+), 66 deletions(-) diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 4b9169499758..d0a667bf018f 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.8.9 + +* Adds `getMedia` and `getMultipleMedia` methods. + ## 0.8.8 * Adds initial support for Windows, macOS, and Linux. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 2c5aa5c3f1a2..33ecc2edee81 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -59,7 +59,7 @@ When under high memory pressure the Android system may kill the MainActivity of the application using the image_picker. On Android the image_picker makes use of the default `Intent.ACTION_GET_CONTENT` or `MediaStore.ACTION_IMAGE_CAPTURE` intents. This means that while the intent is executing the source application -is moved to the background and becomes eligable for cleanup when the system is +is moved to the background and becomes eligible for cleanup when the system is low on memory. When the intent finishes executing, Android will restart the application. Since the data is never returned to the original call use the `ImagePicker.retrieveLostData()` method to retrieve the lost data. For example: @@ -180,6 +180,10 @@ final XFile? galleryVideo = final XFile? cameraVideo = await picker.pickVideo(source: ImageSource.camera); // Pick multiple images. final List images = await picker.pickMultiImage(); +// Pick singe image or video. +final XFile? media = await picker.pickMedia(); +// Pick multiple images and videos. +final List medias = await picker.pickMultipleMedia(); ``` ## Migrating to 0.8.2+ diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index e7c5dae28514..b1431c5c33bb 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; void main() { @@ -38,10 +39,10 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - List? _imageFileList; + List? _mediaFileList; void _setImageFileListFromFile(XFile? value) { - _imageFileList = value == null ? null : [value]; + _mediaFileList = value == null ? null : [value]; } dynamic _pickImageError; @@ -80,8 +81,12 @@ class _MyHomePageState extends State { } } - Future _onImageButtonPressed(ImageSource source, - {required BuildContext context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + ImageSource source, { + required BuildContext context, + bool isMultiImage = false, + bool isMedia = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } @@ -94,14 +99,42 @@ class _MyHomePageState extends State { await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { - final List pickedFileList = await _picker.pickMultiImage( + final List pickedFileList = isMedia + ? await _picker.pickMultipleMedia( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ) + : await _picker.pickMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _mediaFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); + } else if (isMedia) { + await _displayPickImageDialog(context, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final List pickedFileList = []; + final XFile? media = await _picker.pickMedia( maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: quality, ); - setState(() { - _imageFileList = pickedFileList; - }); + if (media != null) { + pickedFileList.add(media); + setState(() { + _mediaFileList = pickedFileList; + }); + } } catch (e) { setState(() { _pickImageError = e; @@ -179,28 +212,34 @@ class _MyHomePageState extends State { if (retrieveError != null) { return retrieveError; } - if (_imageFileList != null) { + if (_mediaFileList != null) { return Semantics( label: 'image_picker_example_picked_images', child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { + final String? mime = lookupMimeType(_mediaFileList![index].path); + // Why network for web? // See https://pub.dev/packages/image_picker_for_web#limitations-on-the-web-platform return Semantics( label: 'image_picker_example_picked_image', child: kIsWeb - ? Image.network(_imageFileList![index].path) - : Image.file( - File(_imageFileList![index].path), - errorBuilder: (BuildContext context, Object error, - StackTrace? stackTrace) => - const Center( - child: Text('This image type is not supported')), - ), + ? Image.network(_mediaFileList![index].path) + : (mime == null || mime.startsWith('image/') + ? Image.file( + File(_mediaFileList![index].path), + errorBuilder: (BuildContext context, Object error, + StackTrace? stackTrace) { + return const Center( + child: + Text('This image type is not supported')); + }, + ) + : _buildInlineVideoPlayer(index)), ); }, - itemCount: _imageFileList!.length, + itemCount: _mediaFileList!.length, ), ); } else if (_pickImageError != null) { @@ -216,6 +255,17 @@ class _MyHomePageState extends State { } } + Widget _buildInlineVideoPlayer(int index) { + final VideoPlayerController controller = + VideoPlayerController.file(File(_mediaFileList![index].path)); + const double volume = kIsWeb ? 0.0 : 1.0; + controller.setVolume(volume); + controller.initialize(); + controller.setLooping(true); + controller.play(); + return Center(child: AspectRatioVideo(controller)); + } + Widget _handlePreview() { if (isVideo) { return _previewVideo(); @@ -239,7 +289,7 @@ class _MyHomePageState extends State { if (response.files == null) { _setImageFileListFromFile(response.file); } else { - _imageFileList = response.files; + _mediaFileList = response.files; } }); } @@ -300,6 +350,39 @@ class _MyHomePageState extends State { child: const Icon(Icons.photo), ), ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + isMedia: true, + ); + }, + heroTag: 'multipleMedia', + tooltip: 'Pick Multiple Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMedia: true, + ); + }, + heroTag: 'media', + tooltip: 'Pick Single Media from gallery', + child: const Icon(Icons.photo_library), + ), + ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( diff --git a/packages/image_picker/image_picker/example/lib/readme_excerpts.dart b/packages/image_picker/image_picker/example/lib/readme_excerpts.dart index e32f4fc84154..15c8185ecf6e 100644 --- a/packages/image_picker/image_picker/example/lib/readme_excerpts.dart +++ b/packages/image_picker/image_picker/example/lib/readme_excerpts.dart @@ -42,6 +42,10 @@ Future> readmePickExample() async { final XFile? cameraVideo = await picker.pickVideo(source: ImageSource.camera); // Pick multiple images. final List images = await picker.pickMultiImage(); + // Pick singe image or video. + final XFile? media = await picker.pickMedia(); + // Pick multiple images and videos. + final List medias = await picker.pickMultipleMedia(); // #enddocregion Pick // Return everything for the sanity check test. @@ -50,7 +54,9 @@ Future> readmePickExample() async { photo, galleryVideo, cameraVideo, - if (images.isEmpty) null else images.first + if (images.isEmpty) null else images.first, + media, + if (medias.isEmpty) null else medias.first, ]; } diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index fc28de420110..4fbeb73be3f6 100644 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -17,7 +17,8 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 + mime: ^1.0.4 video_player: ^2.1.4 dev_dependencies: diff --git a/packages/image_picker/image_picker/example/test/readme_excerpts_test.dart b/packages/image_picker/image_picker/example/test/readme_excerpts_test.dart index 771d5d419de1..512438ce2b5c 100644 --- a/packages/image_picker/image_picker/example/test/readme_excerpts_test.dart +++ b/packages/image_picker/image_picker/example/test/readme_excerpts_test.dart @@ -50,6 +50,13 @@ class FakeImagePicker extends ImagePickerPlatform { return [XFile('multiImage')]; } + @override + Future> getMedia({required MediaOptions options}) async { + return options.allowMultiple + ? [XFile('medias'), XFile('medias')] + : [XFile('media')]; + } + @override Future getVideo( {required ImageSource source, diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 0eb35b4bce99..a558dbd7d55c 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -27,7 +27,7 @@ class ImagePicker { /// Returns a [PickedFile] object wrapping the image that was picked. /// - /// The returned [PickedFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [PickedFile] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// The `source` argument controls where the image comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. @@ -78,7 +78,7 @@ class ImagePicker { /// Returns a [List] object wrapping the images that were picked. /// - /// The returned [List] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [List] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used /// in addition to a size modification, of which the usage is explained below. @@ -115,7 +115,7 @@ class ImagePicker { /// Returns a [PickedFile] object wrapping the video that was picked. /// - /// The returned [PickedFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [PickedFile] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// The [source] argument controls where the video comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. @@ -151,7 +151,7 @@ class ImagePicker { /// Retrieve the lost [PickedFile] when [selectImage] or [selectVideo] failed because the MainActivity is destroyed. (Android only) /// /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. - /// Call this method to retrieve the lost data and process the data according to your APP's business logic. + /// Call this method to retrieve the lost data and process the data according to your app's business logic. /// /// Returns a [LostData] object if successfully retrieved the lost data. The [LostData] object can represent either a /// successful image/video selection, or a failure. @@ -168,7 +168,7 @@ class ImagePicker { /// Returns an [XFile] object wrapping the image that was picked. /// - /// The returned [XFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [XFile] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// The `source` argument controls where the image comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. @@ -217,32 +217,24 @@ class ImagePicker { CameraDevice preferredCameraDevice = CameraDevice.rear, bool requestFullMetadata = true, }) { - if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { - throw ArgumentError.value( - imageQuality, 'imageQuality', 'must be between 0 and 100'); - } - if (maxWidth != null && maxWidth < 0) { - throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); - } - if (maxHeight != null && maxHeight < 0) { - throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); - } + final ImagePickerOptions imagePickerOptions = + ImagePickerOptions.createAndValidate( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + requestFullMetadata: requestFullMetadata, + ); return platform.getImageFromSource( source: source, - options: ImagePickerOptions( - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: imageQuality, - preferredCameraDevice: preferredCameraDevice, - requestFullMetadata: requestFullMetadata, - ), + options: imagePickerOptions, ); } /// Returns a [List] object wrapping the images that were picked. /// - /// The returned [List] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [List] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used /// in addition to a size modification, of which the usage is explained below. @@ -277,22 +269,121 @@ class ImagePicker { int? imageQuality, bool requestFullMetadata = true, }) { - if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { - throw ArgumentError.value( - imageQuality, 'imageQuality', 'must be between 0 and 100'); - } - if (maxWidth != null && maxWidth < 0) { - throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); - } - if (maxHeight != null && maxHeight < 0) { - throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); - } + final ImageOptions imageOptions = ImageOptions.createAndValidate( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + requestFullMetadata: requestFullMetadata, + ); return platform.getMultiImageWithOptions( options: MultiImagePickerOptions( - imageOptions: ImageOptions( + imageOptions: imageOptions, + ), + ); + } + + /// Returns an [XFile] of the image or video that was picked. + /// The image or videos can only come from the gallery. + /// + /// The returned [XFile] is intended to be used within a single app session. + /// Do not save the file path and use it across sessions. + /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and + /// above only support HEIC images if used in addition to a size modification, + /// of which the usage is explained below. + /// + /// This method is not supported in iOS versions lower than 14. + /// + /// If specified, the image will be at most `maxWidth` wide and + /// `maxHeight` tall. Otherwise the image will be returned at it's + /// original width and height. + /// + /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the image with + /// the original quality will be returned. Compression is only supported for certain + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not + /// supported for the image that is picked, a warning message will be logged. + /// + /// Use `requestFullMetadata` (defaults to `true`) to control how much additional + /// information the plugin tries to get. + /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full + /// image metadata which may require extra permission requests on some platforms, + /// such as `Photo Library Usage` permission on iOS. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the photos gallery, plugin is already in use, temporary file could not be + /// created (iOS only), plugin activity could not be allocated (Android only) + /// or due to an unknown error. + /// + /// If no image or video was picked, the return value is null. + Future pickMedia({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + bool requestFullMetadata = true, + }) async { + final List listMedia = await platform.getMedia( + options: MediaOptions( + imageOptions: ImageOptions.createAndValidate( + maxHeight: maxHeight, maxWidth: maxWidth, + imageQuality: imageQuality, + requestFullMetadata: requestFullMetadata, + ), + allowMultiple: false, + ), + ); + + return listMedia.isNotEmpty ? listMedia.first : null; + } + + /// Returns a [List] with the images and/or videos that were picked. + /// The images and videos come from the gallery. + /// + /// The returned [List] is intended to be used within a single app session. + /// Do not save the file paths and use them across sessions. + /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and + /// above only support HEIC images if used in addition to a size modification, + /// of which the usage is explained below. + /// + /// This method is not supported in iOS versions lower than 14. + /// + /// If specified, the images will be at most `maxWidth` wide and + /// `maxHeight` tall. Otherwise the images will be returned at their + /// original width and height. + /// + /// The `imageQuality` argument modifies the quality of the images, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the images with + /// the original quality will be returned. Compression is only supported for certain + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not + /// supported for the image that is picked, a warning message will be logged. + /// + /// Use `requestFullMetadata` (defaults to `true`) to control how much additional + /// information the plugin tries to get. + /// If `requestFullMetadata` is set to `true`, the plugin tries to get the full + /// image metadata which may require extra permission requests on some platforms, + /// such as `Photo Library Usage` permission on iOS. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the photos gallery, plugin is already in use, temporary file could not be + /// created (iOS only), plugin activity could not be allocated (Android only) + /// or due to an unknown error. + /// + /// If no images or videos were picked, the return value is an empty list. + Future> pickMultipleMedia({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + bool requestFullMetadata = true, + }) { + return platform.getMedia( + options: MediaOptions( + allowMultiple: true, + imageOptions: ImageOptions.createAndValidate( maxHeight: maxHeight, + maxWidth: maxWidth, imageQuality: imageQuality, requestFullMetadata: requestFullMetadata, ), @@ -302,7 +393,7 @@ class ImagePicker { /// Returns an [XFile] object wrapping the video that was picked. /// - /// The returned [XFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// The returned [XFile] is intended to be used within a single app session. Do not save the file path and use it across sessions. /// /// The [source] argument controls where the video comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. @@ -338,7 +429,7 @@ class ImagePicker { /// is destroyed. (Android only) /// /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. - /// Call this method to retrieve the lost data and process the data according to your APP's business logic. + /// Call this method to retrieve the lost data and process the data according to your app's business logic. /// /// Returns a [LostDataResponse] object if successfully retrieved the lost data. The [LostDataResponse] object can \ /// represent either a successful image/video selection, or a failure. diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 8b38ba56cae9..69e255c65bab 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.8 +version: 0.8.9 environment: sdk: ">=2.18.0 <4.0.0" @@ -33,7 +33,7 @@ dependencies: image_picker_ios: ^0.8.6+1 image_picker_linux: ^0.2.0 image_picker_macos: ^0.2.0 - image_picker_platform_interface: ^2.7.0 + image_picker_platform_interface: ^2.8.0 image_picker_windows: ^0.2.0 dev_dependencies: diff --git a/packages/image_picker/image_picker/test/image_picker_test.dart b/packages/image_picker/image_picker/test/image_picker_test.dart index 459a383b5d97..4ff5b4e025d0 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -588,14 +588,369 @@ void main() { }); }); - test('supportsImageSource calls through to platform', () async { - final ImagePicker picker = ImagePicker(); - when(mockPlatform.supportsImageSource(any)).thenReturn(true); + group('#Media', () { + setUp(() { + when( + mockPlatform.getMedia( + options: anyNamed('options'), + ), + ).thenAnswer((Invocation _) async => []); + }); + + group('#pickMedia', () { + test('passes the width and height arguments correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMedia(); + await picker.pickMedia( + maxWidth: 10.0, + ); + await picker.pickMedia( + maxHeight: 10.0, + ); + await picker.pickMedia( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickMedia( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickMedia( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickMedia( + maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); + + verifyInOrder([ + mockPlatform.getMedia( + options: argThat( + isInstanceOf(), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(20.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + ]); + }); + + test('does not accept a negative width or height argument', () { + final ImagePicker picker = ImagePicker(); + expect( + () => picker.pickMedia(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickMedia(maxHeight: -1.0), + throwsArgumentError, + ); + }); - final bool supported = picker.supportsImageSource(ImageSource.camera); + test('handles an empty image file response gracefully', () async { + final ImagePicker picker = ImagePicker(); - expect(supported, true); - verify(mockPlatform.supportsImageSource(ImageSource.camera)); + expect(await picker.pickMedia(), isNull); + expect(await picker.pickMedia(), isNull); + }); + + test('full metadata argument defaults to true', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMedia(); + + verify(mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isTrue), + named: 'options', + ), + )); + }); + + test('passes the full metadata argument correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMedia( + requestFullMetadata: false, + ); + + verify(mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isFalse), + named: 'options', + ), + )); + }); + }); + + group('#pickMultipleMedia', () { + test('passes the width and height arguments correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMultipleMedia(); + await picker.pickMultipleMedia( + maxWidth: 10.0, + ); + await picker.pickMultipleMedia( + maxHeight: 10.0, + ); + await picker.pickMultipleMedia( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickMultipleMedia( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickMultipleMedia( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickMultipleMedia( + maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); + + verifyInOrder([ + mockPlatform.getMedia( + options: argThat( + isInstanceOf(), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(20.0)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => + options.imageOptions.maxHeight, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + mockPlatform.getMedia( + options: argThat( + isInstanceOf() + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxWidth', + equals(10.0)) + .having( + (MediaOptions options) => options.imageOptions.maxWidth, + 'maxHeight', + equals(10.0)) + .having( + (MediaOptions options) => + options.imageOptions.imageQuality, + 'imageQuality', + equals(70)), + named: 'options', + ), + ), + ]); + }); + + test('does not accept a negative width or height argument', () { + final ImagePicker picker = ImagePicker(); + expect( + () => picker.pickMultipleMedia(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickMultipleMedia(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles an empty image file response gracefully', () async { + final ImagePicker picker = ImagePicker(); + + expect(await picker.pickMultipleMedia(), isEmpty); + expect(await picker.pickMultipleMedia(), isEmpty); + }); + + test('full metadata argument defaults to true', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMultipleMedia(); + + verify(mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isTrue), + named: 'options', + ), + )); + }); + + test('passes the full metadata argument correctly', () async { + final ImagePicker picker = ImagePicker(); + await picker.pickMultipleMedia( + requestFullMetadata: false, + ); + + verify(mockPlatform.getMedia( + options: argThat( + isInstanceOf().having( + (MediaOptions options) => + options.imageOptions.requestFullMetadata, + 'requestFullMetadata', + isFalse), + named: 'options', + ), + )); + }); + }); + test('supportsImageSource calls through to platform', () async { + final ImagePicker picker = ImagePicker(); + when(mockPlatform.supportsImageSource(any)).thenReturn(true); + + final bool supported = picker.supportsImageSource(ImageSource.camera); + + expect(supported, true); + verify(mockPlatform.supportsImageSource(ImageSource.camera)); + }); }); }); } diff --git a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart index 7336d7d50274..85c9df08c790 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.mocks.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.mocks.dart @@ -165,6 +165,16 @@ class MockImagePickerPlatform extends _i1.Mock returnValue: _i4.Future?>.value(), ) as _i4.Future?>); @override + _i4.Future> getMedia({required _i2.MediaOptions? options}) => + (super.noSuchMethod( + Invocation.method( + #getMedia, + [], + {#options: options}, + ), + returnValue: _i4.Future>.value(<_i5.XFile>[]), + ) as _i4.Future>); + @override _i4.Future<_i5.XFile?> getVideo({ required _i2.ImageSource? source, _i2.CameraDevice? preferredCameraDevice = _i2.CameraDevice.rear, From 7b4b31a8e7dceefb24e48cabd5fc670d69e79c7c Mon Sep 17 00:00:00 2001 From: yk3372 Date: Thu, 15 Jun 2023 13:06:03 +0800 Subject: [PATCH 34/53] [webview_flutter][webview_flutter_android] Add android support for handling geolocation permissions (#3795) This PR Add android support for handling geolocation permissions. [(WebChromeClient.onGeolocationPermissionsShowPrompt)](https://developer.android.com/reference/android/webkit/WebChromeClient#onGeolocationPermissionsShowPrompt(java.lang.String,%20android.webkit.GeolocationPermissions.Callback)) api as a platform callback that notify the host application that web content from the specified origin is attempting to use the Geolocation API. The host application should invoke the specified callback with the desired permission state. Fixes https://github.com/flutter/flutter/issues/27472 --- .../webview_flutter_android/AUTHORS | 1 + .../webview_flutter_android/CHANGELOG.md | 5 + .../GeneratedAndroidWebView.java | 129 +++++++++++++++ ...tionPermissionsCallbackFlutterApiImpl.java | 62 ++++++++ ...ocationPermissionsCallbackHostApiImpl.java | 52 ++++++ .../WebChromeClientFlutterApiImpl.java | 27 ++++ .../WebChromeClientHostApiImpl.java | 12 ++ .../webviewflutter/WebViewFlutterPlugin.java | 4 + .../GeolocationPermissionsCallbackTest.java | 74 +++++++++ .../webviewflutter/WebChromeClientTest.java | 20 ++- .../lib/src/android_proxy.dart | 28 ++-- .../lib/src/android_webview.dart | 78 +++++++++ .../lib/src/android_webview.g.dart | 139 ++++++++++++++++ .../lib/src/android_webview_api_impls.dart | 106 +++++++++++++ .../lib/src/android_webview_controller.dart | 99 ++++++++++++ .../pigeons/android_webview.dart | 36 +++++ .../webview_flutter_android/pubspec.yaml | 2 +- .../android_navigation_delegate_test.dart | 2 + .../test/android_webview_controller_test.dart | 112 +++++++++++-- ...android_webview_controller_test.mocks.dart | 39 +++++ ...oid_webview_cookie_manager_test.mocks.dart | 17 ++ .../test/android_webview_test.dart | 150 +++++++++++++++--- .../test/android_webview_test.mocks.dart | 30 ++++ .../test/test_android_webview.g.dart | 53 +++++++ 24 files changed, 1227 insertions(+), 50 deletions(-) create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackFlutterApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackHostApiImpl.java create mode 100644 packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackTest.java diff --git a/packages/webview_flutter/webview_flutter_android/AUTHORS b/packages/webview_flutter/webview_flutter_android/AUTHORS index 22e2b0ef78fc..b664f363f743 100644 --- a/packages/webview_flutter/webview_flutter_android/AUTHORS +++ b/packages/webview_flutter/webview_flutter_android/AUTHORS @@ -66,4 +66,5 @@ Alex Li Rahul Raj <64.rahulraj@gmail.com> Maurits van Beusekom Nick Bradshaw +Kai Yu diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 3160b8594f6e..adc5034f11df 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.8.0 + +* Adds support for handling geolocation permissions. See + `AndroidWebViewController.setGeolocationPermissionsPromptCallbacks`. + ## 3.7.1 * Removes obsolete null checks on non-nullable values. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java index 4dc43fa1d9db..cc52b0b10888 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java @@ -2589,6 +2589,33 @@ public void onPermissionRequest( new ArrayList(Arrays.asList(instanceIdArg, requestInstanceIdArg)), channelReply -> callback.reply(null)); } + /** Callback to Dart function `WebChromeClient.onGeolocationPermissionsShowPrompt`. */ + public void onGeolocationPermissionsShowPrompt( + @NonNull Long instanceIdArg, + @NonNull Long paramsInstanceIdArg, + @NonNull String originArg, + @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(instanceIdArg, paramsInstanceIdArg, originArg)), + channelReply -> callback.reply(null)); + } + /** Callback to Dart function `WebChromeClient.onGeolocationPermissionsHidePrompt`. */ + public void onGeolocationPermissionsHidePrompt( + @NonNull Long identifierArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsHidePrompt", + getCodec()); + channel.send( + new ArrayList(Collections.singletonList(identifierArg)), + channelReply -> callback.reply(null)); + } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebStorageHostApi { @@ -2839,4 +2866,106 @@ public void create( channelReply -> callback.reply(null)); } } + /** + * Host API for `GeolocationPermissionsCallback`. + * + *

This class may handle instantiating and adding native object instances that are attached to + * a Dart instance or handle method calls on the associated native class or an instance of the + * class. + * + *

See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. + * + *

Generated interface from Pigeon that represents a handler of messages from Flutter. + */ + public interface GeolocationPermissionsCallbackHostApi { + /** Handles Dart method `GeolocationPermissionsCallback.invoke`. */ + void invoke( + @NonNull Long instanceId, + @NonNull String origin, + @NonNull Boolean allow, + @NonNull Boolean retain); + + /** The codec used by GeolocationPermissionsCallbackHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `GeolocationPermissionsCallbackHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup( + @NonNull BinaryMessenger binaryMessenger, + @Nullable GeolocationPermissionsCallbackHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number instanceIdArg = (Number) args.get(0); + String originArg = (String) args.get(1); + Boolean allowArg = (Boolean) args.get(2); + Boolean retainArg = (Boolean) args.get(3); + try { + api.invoke( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), + originArg, + allowArg, + retainArg); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** + * Flutter API for `GeolocationPermissionsCallback`. + * + *

This class may handle instantiating and adding Dart instances that are attached to a native + * instance or receiving callback methods from an overridden native class. + * + *

See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. + * + *

Generated class from Pigeon that represents Flutter messages that can be called from Java. + */ + public static class GeolocationPermissionsCallbackFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public GeolocationPermissionsCallbackFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by GeolocationPermissionsCallbackFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** Create a new Dart instance and add it to the `InstanceManager`. */ + public void create(@NonNull Long instanceIdArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.GeolocationPermissionsCallbackFlutterApi.create", + getCodec()); + channel.send( + new ArrayList(Collections.singletonList(instanceIdArg)), + channelReply -> callback.reply(null)); + } + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackFlutterApiImpl.java new file mode 100644 index 000000000000..32c66c0d9f31 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackFlutterApiImpl.java @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.GeolocationPermissions; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.GeolocationPermissionsCallbackFlutterApi; + +/** + * Flutter API implementation for `GeolocationPermissionsCallback`. + * + *

This class may handle adding native instances that are attached to a Dart instance or passing + * arguments of callbacks methods to a Dart instance. + */ +public class GeolocationPermissionsCallbackFlutterApiImpl { + // To ease adding additional methods, this value is added prematurely. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final BinaryMessenger binaryMessenger; + + private final InstanceManager instanceManager; + private GeolocationPermissionsCallbackFlutterApi api; + + /** + * Constructs a {@link GeolocationPermissionsCallbackFlutterApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public GeolocationPermissionsCallbackFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + api = new GeolocationPermissionsCallbackFlutterApi(binaryMessenger); + } + + /** + * Stores the `GeolocationPermissionsCallback` instance and notifies Dart to create and store a + * new `GeolocationPermissionsCallback` instance that is attached to this one. If `instance` has + * already been added, this method does nothing. + */ + public void create( + @NonNull GeolocationPermissions.Callback instance, + @NonNull GeolocationPermissionsCallbackFlutterApi.Reply callback) { + if (!instanceManager.containsInstance(instance)) { + api.create(instanceManager.addHostCreatedInstance(instance), callback); + } + } + + /** + * Sets the Flutter API used to send messages to Dart. + * + *

This is only visible for testing. + */ + @VisibleForTesting + void setApi(@NonNull GeolocationPermissionsCallbackFlutterApi api) { + this.api = api; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackHostApiImpl.java new file mode 100644 index 000000000000..58981b6e3703 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackHostApiImpl.java @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.GeolocationPermissions; +import androidx.annotation.NonNull; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.GeolocationPermissionsCallbackHostApi; +import java.util.Objects; + +/** + * Host API implementation for `GeolocationPermissionsCallback`. + * + *

This class may handle instantiating and adding native object instances that are attached to a + * Dart instance or handle method calls on the associated native class or an instance of the class. + */ +public class GeolocationPermissionsCallbackHostApiImpl + implements GeolocationPermissionsCallbackHostApi { + // To ease adding additional methods, this value is added prematurely. + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final BinaryMessenger binaryMessenger; + + private final InstanceManager instanceManager; + + /** + * Constructs a {@link GeolocationPermissionsCallbackHostApiImpl}. + * + * @param binaryMessenger used to communicate with Dart over asynchronous messages + * @param instanceManager maintains instances stored to communicate with attached Dart objects + */ + public GeolocationPermissionsCallbackHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + } + + @Override + public void invoke( + @NonNull Long instanceId, + @NonNull String origin, + @NonNull Boolean allow, + @NonNull Boolean retain) { + getGeolocationPermissionsCallbackInstance(instanceId).invoke(origin, allow, retain); + } + + private GeolocationPermissions.Callback getGeolocationPermissionsCallbackInstance( + @NonNull Long identifier) { + return Objects.requireNonNull(instanceManager.getInstance(identifier)); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java index fab34fc212d7..ad5168fa110a 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java @@ -5,6 +5,7 @@ package io.flutter.plugins.webviewflutter; import android.os.Build; +import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.WebChromeClient; import android.webkit.WebView; @@ -72,6 +73,32 @@ public void onShowFileChooser( callback); } + /** Passes arguments from {@link WebChromeClient#onGeolocationPermissionsShowPrompt} to Dart. */ + public void onGeolocationPermissionsShowPrompt( + @NonNull WebChromeClient webChromeClient, + @NonNull String origin, + @NonNull GeolocationPermissions.Callback callback, + @NonNull WebChromeClientFlutterApi.Reply replyCallback) { + new GeolocationPermissionsCallbackFlutterApiImpl(binaryMessenger, instanceManager) + .create(callback, reply -> {}); + onGeolocationPermissionsShowPrompt( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(webChromeClient)), + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(callback)), + origin, + replyCallback); + } + + /** + * Sends a message to Dart to call `WebChromeClient.onGeolocationPermissionsHidePrompt` on the + * Dart object representing `instance`. + */ + public void onGeolocationPermissionsHidePrompt( + @NonNull WebChromeClient instance, @NonNull WebChromeClientFlutterApi.Reply callback) { + super.onGeolocationPermissionsHidePrompt( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(instance)), + callback); + } + /** * Sends a message to Dart to call `WebChromeClient.onPermissionRequest` on the Dart object * representing `instance`. diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java index 38ebcb8932b8..74ea45e5359a 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java @@ -7,6 +7,7 @@ import android.net.Uri; import android.os.Build; import android.os.Message; +import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; @@ -51,6 +52,17 @@ public void onProgressChanged(@NonNull WebView view, int progress) { flutterApi.onProgressChanged(this, view, (long) progress, reply -> {}); } + @Override + public void onGeolocationPermissionsShowPrompt( + @NonNull String origin, @NonNull GeolocationPermissions.Callback callback) { + flutterApi.onGeolocationPermissionsShowPrompt(this, origin, callback, reply -> {}); + } + + @Override + public void onGeolocationPermissionsHidePrompt() { + flutterApi.onGeolocationPermissionsHidePrompt(this, reply -> {}); + } + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @SuppressWarnings("LambdaLast") @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index 79640b90b0b4..305821943e3c 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -17,6 +17,7 @@ import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CookieManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.GeolocationPermissionsCallbackHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.InstanceManagerHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaObjectHostApi; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi; @@ -137,6 +138,9 @@ private void setUp( PermissionRequestHostApi.setup( binaryMessenger, new PermissionRequestHostApiImpl(binaryMessenger, instanceManager)); } + GeolocationPermissionsCallbackHostApi.setup( + binaryMessenger, + new GeolocationPermissionsCallbackHostApiImpl(binaryMessenger, instanceManager)); } @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackTest.java new file mode 100644 index 000000000000..14f8e15bf864 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/GeolocationPermissionsCallbackTest.java @@ -0,0 +1,74 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +import android.webkit.GeolocationPermissions; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.GeolocationPermissionsCallbackFlutterApi; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class GeolocationPermissionsCallbackTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public GeolocationPermissions.Callback mockGeolocationPermissionsCallback; + + @Mock public BinaryMessenger mockBinaryMessenger; + + @Mock public GeolocationPermissionsCallbackFlutterApi mockFlutterApi; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.create(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.stopFinalizationListener(); + } + + @Test + public void invoke() { + final String origin = "testString"; + final boolean allow = true; + final boolean retain = true; + + final long instanceIdentifier = 0; + instanceManager.addDartCreatedInstance(mockGeolocationPermissionsCallback, instanceIdentifier); + + final GeolocationPermissionsCallbackHostApiImpl hostApi = + new GeolocationPermissionsCallbackHostApiImpl(mockBinaryMessenger, instanceManager); + + hostApi.invoke(instanceIdentifier, origin, allow, retain); + + verify(mockGeolocationPermissionsCallback).invoke(origin, allow, retain); + } + + @Test + public void flutterApiCreate() { + final GeolocationPermissionsCallbackFlutterApiImpl flutterApi = + new GeolocationPermissionsCallbackFlutterApiImpl(mockBinaryMessenger, instanceManager); + flutterApi.setApi(mockFlutterApi); + + flutterApi.create(mockGeolocationPermissionsCallback, reply -> {}); + + final long instanceIdentifier = + Objects.requireNonNull( + instanceManager.getIdentifierForStrongReference(mockGeolocationPermissionsCallback)); + verify(mockFlutterApi).create(eq(instanceIdentifier), any()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java index 9a97cd6f37a6..e2d5a444b716 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebChromeClientTest.java @@ -15,6 +15,7 @@ import android.net.Uri; import android.os.Message; +import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; import android.webkit.WebResourceRequest; import android.webkit.WebView; @@ -120,9 +121,24 @@ public void onCreateWindow() { public void onPermissionRequest() { final PermissionRequest mockRequest = mock(PermissionRequest.class); instanceManager.addDartCreatedInstance(mockRequest, 10); - webChromeClient.onPermissionRequest(mockRequest); - verify(mockFlutterApi).onPermissionRequest(eq(webChromeClient), eq(mockRequest), any()); } + + @Test + public void onGeolocationPermissionsShowPrompt() { + final GeolocationPermissions.Callback mockCallback = + mock(GeolocationPermissions.Callback.class); + webChromeClient.onGeolocationPermissionsShowPrompt("https://flutter.dev", mockCallback); + + verify(mockFlutterApi) + .onGeolocationPermissionsShowPrompt( + eq(webChromeClient), eq("https://flutter.dev"), eq(mockCallback), any()); + } + + @Test + public void onGeolocationPermissionsHidePrompt() { + webChromeClient.onGeolocationPermissionsHidePrompt(); + verify(mockFlutterApi).onGeolocationPermissionsHidePrompt(eq(webChromeClient), any()); + } } diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart index 6cbb932532aa..e95de8cfa6f1 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart @@ -28,18 +28,22 @@ class AndroidWebViewProxy { final android_webview.WebView Function() createAndroidWebView; /// Constructs a [android_webview.WebChromeClient]. - final android_webview.WebChromeClient Function({ - void Function(android_webview.WebView webView, int progress)? - onProgressChanged, - Future> Function( - android_webview.WebView webView, - android_webview.FileChooserParams params, - )? onShowFileChooser, - void Function( - android_webview.WebChromeClient instance, - android_webview.PermissionRequest request, - )? onPermissionRequest, - }) createAndroidWebChromeClient; + final android_webview.WebChromeClient Function( + {void Function(android_webview.WebView webView, int progress)? + onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, + Future Function(String origin, + android_webview.GeolocationPermissionsCallback callback)? + onGeolocationPermissionsShowPrompt, + void Function(android_webview.WebChromeClient instance)? + onGeolocationPermissionsHidePrompt}) createAndroidWebChromeClient; /// Constructs a [android_webview.WebViewClient]. final android_webview.WebViewClient Function({ diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart index 85d0ea009f73..85ec6b902c57 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart @@ -59,6 +59,57 @@ class JavaObject with Copyable { } } +/// A callback interface used by the host application to set the Geolocation +/// permission state for an origin. +/// +/// See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. +@immutable +class GeolocationPermissionsCallback extends JavaObject { + /// Instantiates a [GeolocationPermissionsCallback] without creating and + /// attaching to an instance of the associated native class. + /// + /// This should only be used outside of tests by subclasses created by this + /// library or to create a copy. + @protected + GeolocationPermissionsCallback.detached({ + super.binaryMessenger, + super.instanceManager, + }) : _geolocationPermissionsCallbackApi = + GeolocationPermissionsCallbackHostApiImpl( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + super.detached(); + + final GeolocationPermissionsCallbackHostApiImpl + _geolocationPermissionsCallbackApi; + + /// Sets the Geolocation permission state for the supplied origin. + /// + /// [origin]: The origin for which permissions are set. + /// + /// [allow]: Whether or not the origin should be allowed to use the Geolocation API. + /// + /// [retain]: Whether the permission should be retained beyond the lifetime of + /// a page currently being displayed by a WebView. + Future invoke(String origin, bool allow, bool retain) { + return _geolocationPermissionsCallbackApi.invokeFromInstances( + this, + origin, + allow, + retain, + ); + } + + @override + GeolocationPermissionsCallback copy() { + return GeolocationPermissionsCallback.detached( + binaryMessenger: _geolocationPermissionsCallbackApi.binaryMessenger, + instanceManager: _geolocationPermissionsCallbackApi.instanceManager, + ); + } +} + /// An Android View that displays web pages. /// /// **Basic usage** @@ -962,6 +1013,17 @@ class DownloadListener extends JavaObject { } } +/// Responsible for request the Geolocation API. +typedef GeolocationPermissionsShowPrompt = Future Function( + String origin, + GeolocationPermissionsCallback callback, +); + +/// Responsible for request the Geolocation API is Cancel. +typedef GeolocationPermissionsHidePrompt = void Function( + WebChromeClient instance, +); + /// Handles JavaScript dialogs, favicons, titles, and the progress for [WebView]. class WebChromeClient extends JavaObject { /// Constructs a [WebChromeClient]. @@ -969,6 +1031,8 @@ class WebChromeClient extends JavaObject { this.onProgressChanged, this.onShowFileChooser, this.onPermissionRequest, + this.onGeolocationPermissionsShowPrompt, + this.onGeolocationPermissionsHidePrompt, @visibleForTesting super.binaryMessenger, @visibleForTesting super.instanceManager, }) : super.detached() { @@ -986,6 +1050,8 @@ class WebChromeClient extends JavaObject { this.onProgressChanged, this.onShowFileChooser, this.onPermissionRequest, + this.onGeolocationPermissionsShowPrompt, + this.onGeolocationPermissionsHidePrompt, super.binaryMessenger, super.instanceManager, }) : super.detached(); @@ -1020,6 +1086,16 @@ class WebChromeClient extends JavaObject { PermissionRequest request, )? onPermissionRequest; + /// Indicates the client should handle geolocation permissions. + final GeolocationPermissionsShowPrompt? onGeolocationPermissionsShowPrompt; + + /// Notify the host application that a request for Geolocation permissions, + /// made with a previous call to [onGeolocationPermissionsShowPrompt] has been + /// canceled. + final void Function( + WebChromeClient instance, + )? onGeolocationPermissionsHidePrompt; + /// Sets the required synchronous return value for the Java method, /// `WebChromeClient.onShowFileChooser(...)`. /// @@ -1054,6 +1130,8 @@ class WebChromeClient extends JavaObject { return WebChromeClient.detached( onProgressChanged: onProgressChanged, onShowFileChooser: onShowFileChooser, + onGeolocationPermissionsShowPrompt: onGeolocationPermissionsShowPrompt, + onGeolocationPermissionsHidePrompt: onGeolocationPermissionsHidePrompt, binaryMessenger: _api.binaryMessenger, instanceManager: _api.instanceManager, ); diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart index 5a2c56ef16ab..70473bfc13cf 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart @@ -377,6 +377,7 @@ class CookieManagerHostApi { class _WebViewHostApiCodec extends StandardMessageCodec { const _WebViewHostApiCodec(); + @override void writeValue(WriteBuffer buffer, Object? value) { if (value is WebViewPoint) { @@ -1528,6 +1529,7 @@ class WebViewClientHostApi { class _WebViewClientFlutterApiCodec extends StandardMessageCodec { const _WebViewClientFlutterApiCodec(); + @override void writeValue(WriteBuffer buffer, Object? value) { if (value is WebResourceErrorData) { @@ -1990,6 +1992,13 @@ abstract class WebChromeClientFlutterApi { /// Callback to Dart function `WebChromeClient.onPermissionRequest`. void onPermissionRequest(int instanceId, int requestInstanceId); + /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsShowPrompt`. + void onGeolocationPermissionsShowPrompt( + int instanceId, int paramsInstanceId, String origin); + + /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsHidePrompt`. + void onGeolocationPermissionsHidePrompt(int identifier); + static void setup(WebChromeClientFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -2069,6 +2078,53 @@ abstract class WebChromeClientFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt was null, expected non-null int.'); + final int? arg_paramsInstanceId = (args[1] as int?); + assert(arg_paramsInstanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt was null, expected non-null int.'); + final String? arg_origin = (args[2] as String?); + assert(arg_origin != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsShowPrompt was null, expected non-null String.'); + api.onGeolocationPermissionsShowPrompt( + arg_instanceId!, arg_paramsInstanceId!, arg_origin!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsHidePrompt', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsHidePrompt was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onGeolocationPermissionsHidePrompt was null, expected non-null int.'); + api.onGeolocationPermissionsHidePrompt(arg_identifier!); + return; + }); + } + } } } @@ -2129,6 +2185,7 @@ class WebStorageHostApi { class _FileChooserParamsFlutterApiCodec extends StandardMessageCodec { const _FileChooserParamsFlutterApiCodec(); + @override void writeValue(WriteBuffer buffer, Object? value) { if (value is FileChooserModeEnumData) { @@ -2301,3 +2358,85 @@ abstract class PermissionRequestFlutterApi { } } } + +/// Host API for `GeolocationPermissionsCallback`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. +class GeolocationPermissionsCallbackHostApi { + /// Constructor for [GeolocationPermissionsCallbackHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GeolocationPermissionsCallbackHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + /// Handles Dart method `GeolocationPermissionsCallback.invoke`. + Future invoke(int arg_instanceId, String arg_origin, bool arg_allow, + bool arg_retain) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_instanceId, arg_origin, arg_allow, arg_retain]) + as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} + +/// Flutter API for `GeolocationPermissionsCallback`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. +abstract class GeolocationPermissionsCallbackFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int instanceId); + + static void setup(GeolocationPermissionsCallbackFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GeolocationPermissionsCallbackFlutterApi.create', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackFlutterApi.create was null, expected non-null int.'); + api.create(arg_instanceId!); + return; + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart index 7f7c3427ead6..3f8e10b04032 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart @@ -45,6 +45,8 @@ class AndroidWebViewFlutterApis { WebChromeClientFlutterApiImpl? webChromeClientFlutterApi, JavaScriptChannelFlutterApiImpl? javaScriptChannelFlutterApi, FileChooserParamsFlutterApiImpl? fileChooserParamsFlutterApi, + GeolocationPermissionsCallbackFlutterApiImpl? + geolocationPermissionsCallbackFlutterApi, WebViewFlutterApiImpl? webViewFlutterApi, PermissionRequestFlutterApiImpl? permissionRequestFlutterApi, }) { @@ -60,6 +62,9 @@ class AndroidWebViewFlutterApis { javaScriptChannelFlutterApi ?? JavaScriptChannelFlutterApiImpl(); this.fileChooserParamsFlutterApi = fileChooserParamsFlutterApi ?? FileChooserParamsFlutterApiImpl(); + this.geolocationPermissionsCallbackFlutterApi = + geolocationPermissionsCallbackFlutterApi ?? + GeolocationPermissionsCallbackFlutterApiImpl(); this.webViewFlutterApi = webViewFlutterApi ?? WebViewFlutterApiImpl(); this.permissionRequestFlutterApi = permissionRequestFlutterApi ?? PermissionRequestFlutterApiImpl(); @@ -90,6 +95,10 @@ class AndroidWebViewFlutterApis { /// Flutter Api for [FileChooserParams]. late final FileChooserParamsFlutterApiImpl fileChooserParamsFlutterApi; + /// Flutter Api for [GeolocationPermissionsCallback]. + late final GeolocationPermissionsCallbackFlutterApiImpl + geolocationPermissionsCallbackFlutterApi; + /// Flutter Api for [WebView]. late final WebViewFlutterApiImpl webViewFlutterApi; @@ -105,6 +114,8 @@ class AndroidWebViewFlutterApis { WebChromeClientFlutterApi.setup(webChromeClientFlutterApi); JavaScriptChannelFlutterApi.setup(javaScriptChannelFlutterApi); FileChooserParamsFlutterApi.setup(fileChooserParamsFlutterApi); + GeolocationPermissionsCallbackFlutterApi.setup( + geolocationPermissionsCallbackFlutterApi); WebViewFlutterApi.setup(webViewFlutterApi); PermissionRequestFlutterApi.setup(permissionRequestFlutterApi); _haveBeenSetUp = true; @@ -920,6 +931,32 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { return Future>.value(const []); } + @override + void onGeolocationPermissionsShowPrompt( + int instanceId, int paramsInstanceId, String origin) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(instanceId)!; + final GeolocationPermissionsCallback callback = + instanceManager.getInstanceWithWeakReference(paramsInstanceId)! + as GeolocationPermissionsCallback; + final GeolocationPermissionsShowPrompt? onShowPrompt = + instance.onGeolocationPermissionsShowPrompt; + if (onShowPrompt != null) { + onShowPrompt(origin, callback); + } + } + + @override + void onGeolocationPermissionsHidePrompt(int identifier) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + final GeolocationPermissionsHidePrompt? onHidePrompt = + instance.onGeolocationPermissionsHidePrompt; + if (onHidePrompt != null) { + return onHidePrompt(instance); + } + } + @override void onPermissionRequest( int instanceId, @@ -1007,6 +1044,75 @@ class FileChooserParamsFlutterApiImpl extends FileChooserParamsFlutterApi { } } +/// Host api implementation for [GeolocationPermissionsCallback]. +class GeolocationPermissionsCallbackHostApiImpl + extends GeolocationPermissionsCallbackHostApi { + /// Constructs a [GeolocationPermissionsCallbackHostApiImpl]. + GeolocationPermissionsCallbackHostApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager, + super(binaryMessenger: binaryMessenger); + + /// Sends binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with java objects. + final InstanceManager instanceManager; + + /// Helper method to convert instances ids to objects. + Future invokeFromInstances( + GeolocationPermissionsCallback instance, + String origin, + bool allow, + bool retain, + ) { + return invoke( + instanceManager.getIdentifier(instance)!, + origin, + allow, + retain, + ); + } +} + +/// Flutter API implementation for [GeolocationPermissionsCallback]. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +class GeolocationPermissionsCallbackFlutterApiImpl + implements GeolocationPermissionsCallbackFlutterApi { + /// Constructs a [GeolocationPermissionsCallbackFlutterApiImpl]. + GeolocationPermissionsCallbackFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create(int instanceId) { + instanceManager.addHostCreatedInstance( + GeolocationPermissionsCallback.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + ), + instanceId, + ); + } +} + /// Host api implementation for [PermissionRequest]. class PermissionRequestHostApiImpl extends PermissionRequestHostApi { /// Constructs a [PermissionRequestHostApiImpl]. diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart index a6d3eacddbad..151cc81b8237 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart @@ -110,6 +110,33 @@ class AndroidWebViewController extends PlatformWebViewController { } }; }), + onGeolocationPermissionsShowPrompt: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (String origin, + android_webview.GeolocationPermissionsCallback callback) async { + final OnGeolocationPermissionsShowPrompt? onShowPrompt = + weakReference.target?._onGeolocationPermissionsShowPrompt; + if (onShowPrompt != null) { + final GeolocationPermissionsResponse response = await onShowPrompt( + GeolocationPermissionsRequestParams(origin: origin), + ); + callback.invoke(origin, response.allow, response.retain); + } else { + // default don't allow + callback.invoke(origin, false, false); + } + }; + }), + onGeolocationPermissionsHidePrompt: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (android_webview.WebChromeClient instance) { + final OnGeolocationPermissionsHidePrompt? onHidePrompt = + weakReference.target?._onGeolocationPermissionsHidePrompt; + if (onHidePrompt != null) { + onHidePrompt(); + } + }; + }), onShowFileChooser: withWeakReferenceTo( this, (WeakReference weakReference) { @@ -180,6 +207,11 @@ class AndroidWebViewController extends PlatformWebViewController { Future> Function(FileSelectorParams)? _onShowFileSelectorCallback; + + OnGeolocationPermissionsShowPrompt? _onGeolocationPermissionsShowPrompt; + + OnGeolocationPermissionsHidePrompt? _onGeolocationPermissionsHidePrompt; + void Function(PlatformWebViewPermissionRequest)? _onPermissionRequestCallback; /// Whether to enable the platform's webview content debugging tools. @@ -434,6 +466,33 @@ class AndroidWebViewController extends PlatformWebViewController { ) async { _onPermissionRequestCallback = onPermissionRequest; } + + /// Sets the callback that is invoked when the client request handle geolocation permissions. + /// + /// Param [onShowPrompt] notifies the host application that web content from the specified origin is attempting to use the Geolocation API, + /// but no permission state is currently set for that origin. + /// + /// The host application should invoke the specified callback with the desired permission state. + /// See GeolocationPermissions for details. + /// + /// Note that for applications targeting Android N and later SDKs (API level > Build.VERSION_CODES.M) + /// this method is only called for requests originating from secure origins such as https. + /// On non-secure origins geolocation requests are automatically denied. + /// + /// Param [onHidePrompt] notifies the host application that a request for Geolocation permissions, + /// made with a previous call to onGeolocationPermissionsShowPrompt() has been canceled. + /// Any related UI should therefore be hidden. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient#onGeolocationPermissionsShowPrompt(java.lang.String,%20android.webkit.GeolocationPermissions.Callback) + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient#onGeolocationPermissionsHidePrompt() + Future setGeolocationPermissionsPromptCallbacks({ + OnGeolocationPermissionsShowPrompt? onShowPrompt, + OnGeolocationPermissionsHidePrompt? onHidePrompt, + }) async { + _onGeolocationPermissionsShowPrompt = onShowPrompt; + _onGeolocationPermissionsHidePrompt = onHidePrompt; + } } /// Android implementation of [PlatformWebViewPermissionRequest]. @@ -472,6 +531,46 @@ class AndroidWebViewPermissionRequest extends PlatformWebViewPermissionRequest { } } +/// Signature for the `setGeolocationPermissionsPromptCallbacks` callback responsible for request the Geolocation API. +typedef OnGeolocationPermissionsShowPrompt + = Future Function( + GeolocationPermissionsRequestParams request); + +/// Signature for the `setGeolocationPermissionsPromptCallbacks` callback responsible for request the Geolocation API is cancel. +typedef OnGeolocationPermissionsHidePrompt = void Function(); + +/// A request params used by the host application to set the Geolocation permission state for an origin. +@immutable +class GeolocationPermissionsRequestParams { + /// [origin]: The origin for which permissions are set. + const GeolocationPermissionsRequestParams({ + required this.origin, + }); + + /// [origin]: The origin for which permissions are set. + final String origin; +} + +/// A response used by the host application to set the Geolocation permission state for an origin. +@immutable +class GeolocationPermissionsResponse { + /// [allow]: Whether or not the origin should be allowed to use the Geolocation API. + /// + /// [retain]: Whether the permission should be retained beyond the lifetime of + /// a page currently being displayed by a WebView. + const GeolocationPermissionsResponse({ + required this.allow, + required this.retain, + }); + + /// Whether or not the origin should be allowed to use the Geolocation API. + final bool allow; + + /// Whether the permission should be retained beyond the lifetime of + /// a page currently being displayed by a WebView. + final bool retain; +} + /// Mode of how to select files for a file chooser. enum FileSelectorMode { /// Open single file and requires that the file exists before allowing the diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart index a4886b9bc5c4..f75eb3235bde 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -365,6 +365,16 @@ abstract class WebChromeClientFlutterApi { /// Callback to Dart function `WebChromeClient.onPermissionRequest`. void onPermissionRequest(int instanceId, int requestInstanceId); + + /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsShowPrompt`. + void onGeolocationPermissionsShowPrompt( + int instanceId, + int paramsInstanceId, + String origin, + ); + + /// Callback to Dart function `WebChromeClient.onGeolocationPermissionsHidePrompt`. + void onGeolocationPermissionsHidePrompt(int identifier); } @HostApi(dartHostTestHandler: 'TestWebStorageHostApi') @@ -416,3 +426,29 @@ abstract class PermissionRequestFlutterApi { /// Create a new Dart instance and add it to the `InstanceManager`. void create(int instanceId, List resources); } + +/// Host API for `GeolocationPermissionsCallback`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. +@HostApi(dartHostTestHandler: 'TestGeolocationPermissionsCallbackHostApi') +abstract class GeolocationPermissionsCallbackHostApi { + /// Handles Dart method `GeolocationPermissionsCallback.invoke`. + void invoke(int instanceId, String origin, bool allow, bool retain); +} + +/// Flutter API for `GeolocationPermissionsCallback`. +/// +/// This class may handle instantiating and adding Dart instances that are +/// attached to a native instance or receiving callback methods from an +/// overridden native class. +/// +/// See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. +@FlutterApi() +abstract class GeolocationPermissionsCallbackFlutterApi { + /// Create a new Dart instance and add it to the `InstanceManager`. + void create(int instanceId); +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index fb173760ba7d..189843e4f6d6 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.7.1 +version: 3.8.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart index 7d834ba7466d..3f93d46b7705 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart @@ -515,6 +515,8 @@ class CapturingWebChromeClient extends android_webview.WebChromeClient { CapturingWebChromeClient({ super.onProgressChanged, super.onShowFileChooser, + super.onGeolocationPermissionsShowPrompt, + super.onGeolocationPermissionsHidePrompt, super.onPermissionRequest, super.binaryMessenger, super.instanceManager, diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart index 8dcc1ad19831..d8527ca9e0eb 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart @@ -18,6 +18,8 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte import 'android_navigation_delegate_test.dart'; import 'android_webview_controller_test.mocks.dart'; +import 'android_webview_test.mocks.dart' + show MockTestGeolocationPermissionsCallbackHostApi; import 'test_android_webview.g.dart'; @GenerateNiceMocks(>[ @@ -55,6 +57,10 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + android_webview.GeolocationPermissionsShowPrompt? + onGeolocationPermissionsShowPrompt, + android_webview.GeolocationPermissionsHidePrompt? + onGeolocationPermissionsHidePrompt, void Function( android_webview.WebChromeClient instance, android_webview.PermissionRequest request, @@ -73,18 +79,25 @@ void main() { androidWebStorage: mockWebStorage ?? MockWebStorage(), androidWebViewProxy: AndroidWebViewProxy( createAndroidWebChromeClient: createWebChromeClient ?? - ({ - void Function(android_webview.WebView, int)? - onProgressChanged, - Future> Function( - android_webview.WebView webView, - android_webview.FileChooserParams params, - )? onShowFileChooser, - void Function( - android_webview.WebChromeClient instance, - android_webview.PermissionRequest request, - )? onPermissionRequest, - }) => + ( + {void Function(android_webview.WebView, int)? + onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? onShowFileChooser, + void Function( + android_webview.WebChromeClient instance, + android_webview.PermissionRequest request, + )? onPermissionRequest, + Future Function( + String origin, + android_webview.GeolocationPermissionsCallback + callback, + )? onGeolocationPermissionsShowPrompt, + void Function( + android_webview.WebChromeClient instance)? + onGeolocationPermissionsHidePrompt}) => MockWebChromeClient(), createAndroidWebView: () => nonNullMockWebView, createAndroidWebViewClient: ({ @@ -574,6 +587,8 @@ void main() { android_webview.WebView webView, android_webview.FileChooserParams params, )? onShowFileChooser, + dynamic onGeolocationPermissionsShowPrompt, + dynamic onGeolocationPermissionsHidePrompt, dynamic onPermissionRequest, }) { onShowFileChooserCallback = onShowFileChooser!; @@ -609,6 +624,75 @@ void main() { expect(fileSelectorParams.mode, FileSelectorMode.open); }); + test('setGeolocationPermissionsPromptCallbacks', () async { + final MockTestGeolocationPermissionsCallbackHostApi mockApi = + MockTestGeolocationPermissionsCallbackHostApi(); + TestGeolocationPermissionsCallbackHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final android_webview.GeolocationPermissionsCallback testCallback = + android_webview.GeolocationPermissionsCallback.detached( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(testCallback, instanceIdentifier); + + late final Future Function(String origin, + android_webview.GeolocationPermissionsCallback callback) + onGeoPermissionHandle; + late final void Function(android_webview.WebChromeClient instance) + onGeoPermissionHidePromptHandle; + + final MockWebChromeClient mockWebChromeClient = MockWebChromeClient(); + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: ({ + dynamic onProgressChanged, + dynamic onShowFileChooser, + Future Function(String origin, + android_webview.GeolocationPermissionsCallback callback)? + onGeolocationPermissionsShowPrompt, + void Function(android_webview.WebChromeClient instance)? + onGeolocationPermissionsHidePrompt, + dynamic onPermissionRequest, + }) { + onGeoPermissionHandle = onGeolocationPermissionsShowPrompt!; + onGeoPermissionHidePromptHandle = onGeolocationPermissionsHidePrompt!; + return mockWebChromeClient; + }, + ); + + String testValue = 'origin'; + const String allowOrigin = 'https://www.allow.com'; + bool isAllow = false; + + late final GeolocationPermissionsResponse response; + controller.setGeolocationPermissionsPromptCallbacks( + onShowPrompt: (GeolocationPermissionsRequestParams request) async { + isAllow = request.origin == allowOrigin; + response = + GeolocationPermissionsResponse(allow: isAllow, retain: isAllow); + return response; + }, + onHidePrompt: () { + testValue = 'changed'; + }, + ); + + await onGeoPermissionHandle( + allowOrigin, + testCallback, + ); + + expect(isAllow, true); + + onGeoPermissionHidePromptHandle(mockWebChromeClient); + expect(testValue, 'changed'); + }); + test('setOnPlatformPermissionRequest', () async { late final void Function( android_webview.WebChromeClient instance, @@ -620,6 +704,8 @@ void main() { createWebChromeClient: ({ dynamic onProgressChanged, dynamic onShowFileChooser, + dynamic onGeolocationPermissionsShowPrompt, + dynamic onGeolocationPermissionsHidePrompt, void Function( android_webview.WebChromeClient instance, android_webview.PermissionRequest request, @@ -670,6 +756,8 @@ void main() { createWebChromeClient: ({ dynamic onProgressChanged, dynamic onShowFileChooser, + dynamic onGeolocationPermissionsShowPrompt, + dynamic onGeolocationPermissionsHidePrompt, void Function( android_webview.WebChromeClient instance, android_webview.PermissionRequest request, diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart index 093312e06e1b..f6e7124c4d95 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart @@ -719,6 +719,23 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i9.Future.value(), returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); + @override + _i9.Future setGeolocationPermissionsPromptCallbacks({ + _i8.OnGeolocationPermissionsShowPrompt? onShowPrompt, + _i8.OnGeolocationPermissionsHidePrompt? onHidePrompt, + }) => + (super.noSuchMethod( + Invocation.method( + #setGeolocationPermissionsPromptCallbacks, + [], + { + #onShowPrompt: onShowPrompt, + #onHidePrompt: onHidePrompt, + }, + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); } /// A class which mocks [AndroidWebViewProxy]. @@ -740,6 +757,11 @@ class MockAndroidWebViewProxy extends _i1.Mock ) as _i2.WebView Function()); @override _i2.WebChromeClient Function({ + void Function(_i2.WebChromeClient)? onGeolocationPermissionsHidePrompt, + _i9.Future Function( + String, + _i2.GeolocationPermissionsCallback, + )? onGeolocationPermissionsShowPrompt, void Function( _i2.WebChromeClient, _i2.PermissionRequest, @@ -755,6 +777,12 @@ class MockAndroidWebViewProxy extends _i1.Mock }) get createAndroidWebChromeClient => (super.noSuchMethod( Invocation.getter(#createAndroidWebChromeClient), returnValue: ({ + void Function(_i2.WebChromeClient)? + onGeolocationPermissionsHidePrompt, + _i9.Future Function( + String, + _i2.GeolocationPermissionsCallback, + )? onGeolocationPermissionsShowPrompt, void Function( _i2.WebChromeClient, _i2.PermissionRequest, @@ -773,6 +801,12 @@ class MockAndroidWebViewProxy extends _i1.Mock Invocation.getter(#createAndroidWebChromeClient), ), returnValueForMissingStub: ({ + void Function(_i2.WebChromeClient)? + onGeolocationPermissionsHidePrompt, + _i9.Future Function( + String, + _i2.GeolocationPermissionsCallback, + )? onGeolocationPermissionsShowPrompt, void Function( _i2.WebChromeClient, _i2.PermissionRequest, @@ -791,6 +825,11 @@ class MockAndroidWebViewProxy extends _i1.Mock Invocation.getter(#createAndroidWebChromeClient), ), ) as _i2.WebChromeClient Function({ + void Function(_i2.WebChromeClient)? onGeolocationPermissionsHidePrompt, + _i9.Future Function( + String, + _i2.GeolocationPermissionsCallback, + )? onGeolocationPermissionsShowPrompt, void Function( _i2.WebChromeClient, _i2.PermissionRequest, diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart index 9f4aa1dfc706..ee0b48188ef6 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_cookie_manager_test.mocks.dart @@ -454,6 +454,23 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i5.Future.value(), returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); + @override + _i5.Future setGeolocationPermissionsPromptCallbacks({ + _i6.OnGeolocationPermissionsShowPrompt? onShowPrompt, + _i6.OnGeolocationPermissionsHidePrompt? onHidePrompt, + }) => + (super.noSuchMethod( + Invocation.method( + #setGeolocationPermissionsPromptCallbacks, + [], + { + #onShowPrompt: onShowPrompt, + #onHidePrompt: onHidePrompt, + }, + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } /// A class which mocks [TestInstanceManagerHostApi]. diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart index 922d7c78bbca..995694fee029 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart @@ -19,6 +19,7 @@ import 'test_android_webview.g.dart'; JavaScriptChannel, TestCookieManagerHostApi, TestDownloadListenerHostApi, + TestGeolocationPermissionsCallbackHostApi, TestInstanceManagerHostApi, TestJavaObjectHostApi, TestJavaScriptChannelHostApi, @@ -887,6 +888,28 @@ void main() { expect(result, containsAllInOrder([mockWebView, 76])); }); + test('onGeolocationPermissionsShowPrompt', () async { + const String origin = 'https://www.example.com'; + final GeolocationPermissionsCallback callback = + GeolocationPermissionsCallback.detached(); + final int paramsId = instanceManager.addDartCreatedInstance(callback); + late final GeolocationPermissionsCallback outerCallback; + when(mockWebChromeClient.onGeolocationPermissionsShowPrompt).thenReturn( + (String origin, GeolocationPermissionsCallback callback) async { + outerCallback = callback; + }, + ); + flutterApi.onGeolocationPermissionsShowPrompt( + mockWebChromeClientInstanceId, + paramsId, + origin, + ); + await expectLater( + outerCallback, + callback, + ); + }); + test('onShowFileChooser', () async { late final List result; when(mockWebChromeClient.onShowFileChooser).thenReturn( @@ -1019,32 +1042,59 @@ void main() { }); }); - group('FileChooserParams', () { - test('FlutterApi create', () { - final InstanceManager instanceManager = InstanceManager( - onWeakReferenceRemoved: (_) {}, - ); + test('onGeolocationPermissionsHidePrompt', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); - final FileChooserParamsFlutterApiImpl flutterApi = - FileChooserParamsFlutterApiImpl( - instanceManager: instanceManager, - ); + const int instanceIdentifier = 0; + late final List callbackParameters; + final WebChromeClient instance = WebChromeClient.detached( + onGeolocationPermissionsHidePrompt: (WebChromeClient instance) { + callbackParameters = [instance]; + }, + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); - flutterApi.create( - 0, - false, - const ['my', 'list'], - FileChooserModeEnumData(value: FileChooserMode.openMultiple), - 'filenameHint', - ); + final WebChromeClientFlutterApiImpl flutterApi = + WebChromeClientFlutterApiImpl(instanceManager: instanceManager); - final FileChooserParams instance = instanceManager - .getInstanceWithWeakReference(0)! as FileChooserParams; - expect(instance.isCaptureEnabled, false); - expect(instance.acceptTypes, const ['my', 'list']); - expect(instance.mode, FileChooserMode.openMultiple); - expect(instance.filenameHint, 'filenameHint'); - }); + flutterApi.onGeolocationPermissionsHidePrompt(instanceIdentifier); + + expect(callbackParameters, [instance]); + }); + + test('copy', () { + expect(WebChromeClient.detached().copy(), isA()); + }); + }); + + group('FileChooserParams', () { + test('FlutterApi create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final FileChooserParamsFlutterApiImpl flutterApi = + FileChooserParamsFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create( + 0, + false, + const ['my', 'list'], + FileChooserModeEnumData(value: FileChooserMode.openMultiple), + 'filenameHint', + ); + + final FileChooserParams instance = + instanceManager.getInstanceWithWeakReference(0)! as FileChooserParams; + expect(instance.isCaptureEnabled, false); + expect(instance.acceptTypes, const ['my', 'list']); + expect(instance.mode, FileChooserMode.openMultiple); + expect(instance.filenameHint, 'filenameHint'); }); }); @@ -1233,6 +1283,60 @@ void main() { verify(mockApi.deny(instanceIdentifier)); }); + }); + + group('GeolocationPermissionsCallback', () { + tearDown(() { + TestGeolocationPermissionsCallbackHostApi.setup(null); + }); + + test('invoke', () async { + final MockTestGeolocationPermissionsCallbackHostApi mockApi = + MockTestGeolocationPermissionsCallbackHostApi(); + TestGeolocationPermissionsCallbackHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final GeolocationPermissionsCallback instance = + GeolocationPermissionsCallback.detached( + instanceManager: instanceManager, + ); + const int instanceIdentifier = 0; + instanceManager.addHostCreatedInstance(instance, instanceIdentifier); + + const String origin = 'testString'; + const bool allow = true; + const bool retain = true; + + await instance.invoke( + origin, + allow, + retain, + ); + + verify(mockApi.invoke(instanceIdentifier, origin, allow, retain)); + }); + + test('Geolocation FlutterAPI create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final GeolocationPermissionsCallbackFlutterApiImpl api = + GeolocationPermissionsCallbackFlutterApiImpl( + instanceManager: instanceManager, + ); + + const int instanceIdentifier = 0; + api.create(instanceIdentifier); + + expect( + instanceManager.getInstanceWithWeakReference(instanceIdentifier), + isA(), + ); + }); test('FlutterAPI create', () { final InstanceManager instanceManager = InstanceManager( diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart index 3b55d856d05b..a7f825fa16c8 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart @@ -333,6 +333,36 @@ class MockTestDownloadListenerHostApi extends _i1.Mock ); } +/// A class which mocks [TestGeolocationPermissionsCallbackHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestGeolocationPermissionsCallbackHostApi extends _i1.Mock + implements _i6.TestGeolocationPermissionsCallbackHostApi { + MockTestGeolocationPermissionsCallbackHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void invoke( + int? instanceId, + String? origin, + bool? allow, + bool? retain, + ) => + super.noSuchMethod( + Invocation.method( + #invoke, + [ + instanceId, + origin, + allow, + retain, + ], + ), + returnValueForMissingStub: null, + ); +} + /// A class which mocks [TestInstanceManagerHostApi]. /// /// See the documentation for Mockito's code generation for more information. diff --git a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart index 20b084e40c3c..98060e042f9b 100644 --- a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart +++ b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart @@ -217,6 +217,7 @@ abstract class TestCookieManagerHostApi { class _TestWebViewHostApiCodec extends StandardMessageCodec { const _TestWebViewHostApiCodec(); + @override void writeValue(WriteBuffer buffer, Object? value) { if (value is WebViewPoint) { @@ -1714,3 +1715,55 @@ abstract class TestPermissionRequestHostApi { } } } + +/// Host API for `GeolocationPermissionsCallback`. +/// +/// This class may handle instantiating and adding native object instances that +/// are attached to a Dart instance or handle method calls on the associated +/// native class or an instance of the class. +/// +/// See https://developer.android.com/reference/android/webkit/GeolocationPermissions.Callback. +abstract class TestGeolocationPermissionsCallbackHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + /// Handles Dart method `GeolocationPermissionsCallback.invoke`. + void invoke(int instanceId, String origin, bool allow, bool retain); + + static void setup(TestGeolocationPermissionsCallbackHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke was null, expected non-null int.'); + final String? arg_origin = (args[1] as String?); + assert(arg_origin != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke was null, expected non-null String.'); + final bool? arg_allow = (args[2] as bool?); + assert(arg_allow != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke was null, expected non-null bool.'); + final bool? arg_retain = (args[3] as bool?); + assert(arg_retain != null, + 'Argument for dev.flutter.pigeon.GeolocationPermissionsCallbackHostApi.invoke was null, expected non-null bool.'); + api.invoke(arg_instanceId!, arg_origin!, arg_allow!, arg_retain!); + return []; + }); + } + } + } +} From c3faaddf48918192b81dd375c5f6d053af432161 Mon Sep 17 00:00:00 2001 From: Baptiste DUPUCH Date: Thu, 15 Jun 2023 08:49:19 +0200 Subject: [PATCH 35/53] [webview_flutter] Add support for limitsNavigationsToAppBoundDomains (#4026) `WebKitWebViewControllerCreationParams` doesn't allow the configuration of limitsNavigationsToAppBoundDomains https://webkit.org/blog/10882/app-bound-domains/ It's very useful after ios14 if you want users to be able to call runJavaScript on a non local web-page. This PR adds support for it --- .../webview_flutter_wkwebview/CHANGELOG.md | 5 +++ .../FWFWebViewConfigurationHostApiTests.m | 18 +++++++++++ .../ios/Classes/FWFGeneratedWebKitApis.h | 6 +++- .../ios/Classes/FWFGeneratedWebKitApis.m | 30 +++++++++++++++++- .../Classes/FWFWebViewConfigurationHostApi.m | 17 ++++++++++ .../lib/src/common/web_kit.g.dart | 26 +++++++++++++++- .../lib/src/web_kit/web_kit.dart | 14 +++++++++ .../lib/src/web_kit/web_kit_api_impls.dart | 11 +++++++ .../lib/src/webkit_webview_controller.dart | 13 ++++++++ .../pigeons/web_kit.dart | 5 +++ .../webview_flutter_wkwebview/pubspec.yaml | 2 +- .../test/src/common/test_web_kit.g.dart | 31 ++++++++++++++++++- .../test/src/ui_kit/ui_kit_test.mocks.dart | 15 +++++++++ .../test/src/web_kit/web_kit_test.dart | 8 +++++ .../test/src/web_kit/web_kit_test.mocks.dart | 15 +++++++++ .../test/webkit_webview_controller_test.dart | 18 +++++++++++ .../webkit_webview_controller_test.mocks.dart | 10 ++++++ .../webkit_webview_widget_test.mocks.dart | 10 ++++++ 18 files changed, 249 insertions(+), 5 deletions(-) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index 566f23fb67be..69c700f52285 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.5.0 + +* Adds support to limit navigation to pages within the app’s domain. See + `WebKitWebViewControllerCreationParams.limitsNavigationsToAppBoundDomains`. + ## 3.4.4 * Removes obsolete null checks on non-nullable values. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewConfigurationHostApiTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewConfigurationHostApiTests.m index 2ec74d0522dd..98be6dfe9e2b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewConfigurationHostApiTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewConfigurationHostApiTests.m @@ -62,6 +62,24 @@ - (void)testSetAllowsInlineMediaPlayback { XCTAssertNil(error); } +- (void)testSetLimitsNavigationsToAppBoundDomains API_AVAILABLE(ios(14.0)) { + WKWebViewConfiguration *mockWebViewConfiguration = OCMClassMock([WKWebViewConfiguration class]); + + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:mockWebViewConfiguration withIdentifier:0]; + + FWFWebViewConfigurationHostApiImpl *hostAPI = [[FWFWebViewConfigurationHostApiImpl alloc] + initWithBinaryMessenger:OCMProtocolMock(@protocol(FlutterBinaryMessenger)) + instanceManager:instanceManager]; + + FlutterError *error; + [hostAPI setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier:@0 + isLimited:@NO + error:&error]; + OCMVerify([mockWebViewConfiguration setLimitsNavigationsToAppBoundDomains:NO]); + XCTAssertNil(error); +} + - (void)testSetMediaTypesRequiringUserActionForPlayback { WKWebViewConfiguration *mockWebViewConfiguration = OCMClassMock([WKWebViewConfiguration class]); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h index 45b1e42a355a..24b6346e14eb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.h @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -480,6 +480,10 @@ NSObject *FWFWKWebViewConfigurationHostApiGetCodec(void); error: (FlutterError *_Nullable *_Nonnull) error; +- (void)setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier:(NSNumber *)identifier + isLimited:(NSNumber *)limit + error:(FlutterError *_Nullable + *_Nonnull)error; - (void) setMediaTypesRequiresUserActionForConfigurationWithIdentifier:(NSNumber *)identifier forTypes: diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m index 3a5dff6a5d59..fc28c28cc553 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFGeneratedWebKitApis.m @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "FWFGeneratedWebKitApis.h" @@ -1002,6 +1002,34 @@ void FWFWKWebViewConfigurationHostApiSetup(id binaryMess [channel setMessageHandler:nil]; } } + { + FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] + initWithName:@"dev.flutter.pigeon.WKWebViewConfigurationHostApi." + @"setLimitsNavigationsToAppBoundDomains" + binaryMessenger:binaryMessenger + codec:FWFWKWebViewConfigurationHostApiGetCodec()]; + if (api) { + NSCAssert([api respondsToSelector:@selector + (setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier: + isLimited:error:)], + @"FWFWKWebViewConfigurationHostApi api (%@) doesn't respond to " + @"@selector(setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier:" + @"isLimited:error:)", + api); + [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { + NSArray *args = message; + NSNumber *arg_identifier = GetNullableObjectAtIndex(args, 0); + NSNumber *arg_limit = GetNullableObjectAtIndex(args, 1); + FlutterError *error; + [api setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier:arg_identifier + isLimited:arg_limit + error:&error]; + callback(wrapResult(nil, error)); + }]; + } else { + [channel setMessageHandler:nil]; + } + } { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] initWithName:@"dev.flutter.pigeon.WKWebViewConfigurationHostApi." diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m index 987d3f45ff2c..762c07b5abe6 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewConfigurationHostApi.m @@ -104,6 +104,23 @@ - (void)setAllowsInlineMediaPlaybackForConfigurationWithIdentifier:(nonnull NSNu setAllowsInlineMediaPlayback:allow.boolValue]; } +- (void)setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier: + (nonnull NSNumber *)identifier + isLimited: + (nonnull NSNumber *)limit + error:(FlutterError *_Nullable + *_Nonnull)error { + if (@available(iOS 14, *)) { + [[self webViewConfigurationForIdentifier:identifier] + setLimitsNavigationsToAppBoundDomains:limit.boolValue]; + } else { + *error = [FlutterError + errorWithCode:@"FWFUnsupportedVersionError" + message:@"setLimitsNavigationsToAppBoundDomains is only supported on versions 14+." + details:nil]; + } +} + - (void) setMediaTypesRequiresUserActionForConfigurationWithIdentifier:(nonnull NSNumber *)identifier forTypes: diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart index 2ce5055d056c..0f3547a5a44f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import @@ -1067,6 +1067,30 @@ class WKWebViewConfigurationHostApi { } } + Future setLimitsNavigationsToAppBoundDomains( + int arg_identifier, bool arg_limit) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setLimitsNavigationsToAppBoundDomains', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_limit]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + Future setMediaTypesRequiringUserActionForPlayback(int arg_identifier, List arg_types) async { final BasicMessageChannel channel = BasicMessageChannel( diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart index 070f554a5d5a..31cbbf3e1cd7 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit.dart @@ -678,6 +678,20 @@ class WKWebViewConfiguration extends NSObject { ); } + /// Indicates whether the web view limits navigation to pages within the app’s domain. + /// + /// When navigation is limited, Javascript evaluation is unrestricted. + /// See https://webkit.org/blog/10882/app-bound-domains/ + /// + /// Sets [WKWebViewConfiguration.limitsNavigationsToAppBoundDomains](https://developer.apple.com/documentation/webkit/wkwebviewconfiguration/3585117-limitsnavigationstoappbounddomai?language=objc). + Future setLimitsNavigationsToAppBoundDomains(bool limit) { + return _webViewConfigurationApi + .setLimitsNavigationsToAppBoundDomainsForInstances( + this, + limit, + ); + } + /// The media types that require a user gesture to begin playing. /// /// Use [WKAudiovisualMediaType.none] to indicate that no user gestures are diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart index 07a32aee8d33..daba854763a7 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart @@ -629,6 +629,17 @@ class WKWebViewConfigurationHostApiImpl extends WKWebViewConfigurationHostApi { ); } + /// Calls [setLimitsNavigationsToAppBoundDomains] with the ids of the provided object instances. + Future setLimitsNavigationsToAppBoundDomainsForInstances( + WKWebViewConfiguration instance, + bool limit, + ) { + return setLimitsNavigationsToAppBoundDomains( + instanceManager.getIdentifier(instance)!, + limit, + ); + } + /// Calls [setMediaTypesRequiringUserActionForPlayback] with the ids of the provided object instances. Future setMediaTypesRequiringUserActionForPlaybackForInstances( WKWebViewConfiguration instance, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index e49d41257e83..b0666e6dd7cd 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -48,6 +48,7 @@ class WebKitWebViewControllerCreationParams PlaybackMediaTypes.video, }, this.allowsInlineMediaPlayback = false, + this.limitsNavigationsToAppBoundDomains = false, @visibleForTesting InstanceManager? instanceManager, }) : _instanceManager = instanceManager ?? NSObject.globalInstanceManager { _configuration = webKitProxy.createWebViewConfiguration( @@ -68,6 +69,8 @@ class WebKitWebViewControllerCreationParams ); } _configuration.setAllowsInlineMediaPlayback(allowsInlineMediaPlayback); + _configuration.setLimitsNavigationsToAppBoundDomains( + limitsNavigationsToAppBoundDomains); } /// Constructs a [WebKitWebViewControllerCreationParams] using a @@ -83,11 +86,14 @@ class WebKitWebViewControllerCreationParams PlaybackMediaTypes.video, }, bool allowsInlineMediaPlayback = false, + bool limitsNavigationsToAppBoundDomains = false, @visibleForTesting InstanceManager? instanceManager, }) : this( webKitProxy: webKitProxy, mediaTypesRequiringUserAction: mediaTypesRequiringUserAction, allowsInlineMediaPlayback: allowsInlineMediaPlayback, + limitsNavigationsToAppBoundDomains: + limitsNavigationsToAppBoundDomains, instanceManager: instanceManager, ); @@ -104,6 +110,13 @@ class WebKitWebViewControllerCreationParams /// Defaults to false. final bool allowsInlineMediaPlayback; + /// Whether to limit navigation to configured domains. + /// + /// See https://webkit.org/blog/10882/app-bound-domains/ + /// (Only available for iOS > 14.0) + /// Defaults to false. + final bool limitsNavigationsToAppBoundDomains; + /// Handles constructing objects and calling static methods for the WebKit /// native library. @visibleForTesting diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart index 20d4ee41f4fe..ac13958089cd 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart @@ -416,6 +416,11 @@ abstract class WKWebViewConfigurationHostApi { ) void setAllowsInlineMediaPlayback(int identifier, bool allow); + @ObjCSelector( + 'setLimitsNavigationsToAppBoundDomainsForConfigurationWithIdentifier:isLimited:', + ) + void setLimitsNavigationsToAppBoundDomains(int identifier, bool limit); + @ObjCSelector( 'setMediaTypesRequiresUserActionForConfigurationWithIdentifier:forTypes:', ) diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index db7407ba56f9..9056fd9ecae4 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/packages/tree/main/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.4.4 +version: 3.5.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart index ff09d4401ee5..faf01239918d 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v9.2.4), do not edit directly. +// Autogenerated from Pigeon (v9.2.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import // ignore_for_file: avoid_relative_lib_imports @@ -369,6 +369,8 @@ abstract class TestWKWebViewConfigurationHostApi { void setAllowsInlineMediaPlayback(int identifier, bool allow); + void setLimitsNavigationsToAppBoundDomains(int identifier, bool limit); + void setMediaTypesRequiringUserActionForPlayback( int identifier, List types); @@ -448,6 +450,33 @@ abstract class TestWKWebViewConfigurationHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setLimitsNavigationsToAppBoundDomains', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setLimitsNavigationsToAppBoundDomains was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setLimitsNavigationsToAppBoundDomains was null, expected non-null int.'); + final bool? arg_limit = (args[1] as bool?); + assert(arg_limit != null, + 'Argument for dev.flutter.pigeon.WKWebViewConfigurationHostApi.setLimitsNavigationsToAppBoundDomains was null, expected non-null bool.'); + api.setLimitsNavigationsToAppBoundDomains( + arg_identifier!, arg_limit!); + return []; + }); + } + } { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WKWebViewConfigurationHostApi.setMediaTypesRequiringUserActionForPlayback', diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart index 355d69e7dc90..8a9cc199885b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart @@ -69,6 +69,21 @@ class MockTestWKWebViewConfigurationHostApi extends _i1.Mock returnValueForMissingStub: null, ); @override + void setLimitsNavigationsToAppBoundDomains( + int? identifier, + bool? limit, + ) => + super.noSuchMethod( + Invocation.method( + #setLimitsNavigationsToAppBoundDomains, + [ + identifier, + limit, + ], + ), + returnValueForMissingStub: null, + ); + @override void setMediaTypesRequiringUserActionForPlayback( int? identifier, List<_i3.WKAudiovisualMediaTypeEnumData?>? types, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart index e2d53bc9fec3..ac266ff79ebb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart @@ -441,6 +441,14 @@ void main() { )); }); + test('limitsNavigationsToAppBoundDomains', () { + webViewConfiguration.setLimitsNavigationsToAppBoundDomains(true); + verify(mockPlatformHostApi.setLimitsNavigationsToAppBoundDomains( + instanceManager.getIdentifier(webViewConfiguration), + true, + )); + }); + test('mediaTypesRequiringUserActionForPlayback', () { webViewConfiguration.setMediaTypesRequiringUserActionForPlayback( { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart index 601964044dea..0291867aaf16 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart @@ -298,6 +298,21 @@ class MockTestWKWebViewConfigurationHostApi extends _i1.Mock returnValueForMissingStub: null, ); @override + void setLimitsNavigationsToAppBoundDomains( + int? identifier, + bool? limit, + ) => + super.noSuchMethod( + Invocation.method( + #setLimitsNavigationsToAppBoundDomains, + [ + identifier, + limit, + ], + ), + returnValueForMissingStub: null, + ); + @override void setMediaTypesRequiringUserActionForPlayback( int? identifier, List<_i4.WKAudiovisualMediaTypeEnumData?>? types, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index 7e61d36ec7ad..4e56c21dea87 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -140,6 +140,24 @@ void main() { ); }); + test('limitsNavigationsToAppBoundDomains', () { + final MockWKWebViewConfiguration mockConfiguration = + MockWKWebViewConfiguration(); + + WebKitWebViewControllerCreationParams( + webKitProxy: WebKitProxy( + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, + ), + limitsNavigationsToAppBoundDomains: true, + ); + + verify( + mockConfiguration.setLimitsNavigationsToAppBoundDomains(true), + ); + }); + test('mediaTypesRequiringUserAction', () { final MockWKWebViewConfiguration mockConfiguration = MockWKWebViewConfiguration(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart index 9eb03971e828..de787a1e5e28 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.mocks.dart @@ -848,6 +848,16 @@ class MockWKWebViewConfiguration extends _i1.Mock returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override + _i6.Future setLimitsNavigationsToAppBoundDomains(bool? limit) => + (super.noSuchMethod( + Invocation.method( + #setLimitsNavigationsToAppBoundDomains, + [limit], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + @override _i6.Future setMediaTypesRequiringUserActionForPlayback( Set<_i5.WKAudiovisualMediaType>? types) => (super.noSuchMethod( diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart index fe86de77af0c..b171e28a3bfb 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.mocks.dart @@ -176,6 +176,16 @@ class MockWKWebViewConfiguration extends _i1.Mock returnValueForMissingStub: _i3.Future.value(), ) as _i3.Future); @override + _i3.Future setLimitsNavigationsToAppBoundDomains(bool? limit) => + (super.noSuchMethod( + Invocation.method( + #setLimitsNavigationsToAppBoundDomains, + [limit], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override _i3.Future setMediaTypesRequiringUserActionForPlayback( Set<_i2.WKAudiovisualMediaType>? types) => (super.noSuchMethod( From fa2e8a052817d69bd4dd9602a6a5ea82affd29e3 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 15 Jun 2023 09:57:35 -0400 Subject: [PATCH 36/53] [tool] Support code excerpts for any .md file (#4212) Updates `update-excerpts` to support any top-level .md file (other than CHANGELOG.md), rather than just README.md. This is useful for supplemental content, such as migration guides linked from the main README file. Also makes some small improvements to the error messaging: - The list of incorrect files is now relative to, and restricted to, the package. This makes the error message simpler, and ensures that changed files in other packages don't get listed. - Adds a link to the relevant wiki docs, since this has been a source of confusion for newer contributors. --- .../tool/lib/src/update_excerpts_command.dart | 43 +++++++++----- .../test/update_excerpts_command_test.dart | 57 +++++++++++++++++-- 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/script/tool/lib/src/update_excerpts_command.dart b/script/tool/lib/src/update_excerpts_command.dart index 8583e4993c74..d50082377f86 100644 --- a/script/tool/lib/src/update_excerpts_command.dart +++ b/script/tool/lib/src/update_excerpts_command.dart @@ -15,7 +15,7 @@ import 'common/package_looping_command.dart'; import 'common/process_runner.dart'; import 'common/repository_package.dart'; -/// A command to update README code excerpts from code files. +/// A command to update .md code excerpts from code files. class UpdateExcerptsCommand extends PackageLoopingCommand { /// Creates a excerpt updater command instance. UpdateExcerptsCommand( @@ -51,7 +51,7 @@ class UpdateExcerptsCommand extends PackageLoopingCommand { final String name = 'update-excerpts'; @override - final String description = 'Updates code excerpts in README.md files, based ' + final String description = 'Updates code excerpts in .md files, based ' 'on code from code files, via code-excerpt'; @override @@ -105,13 +105,16 @@ class UpdateExcerptsCommand extends PackageLoopingCommand { } if (getBoolArg(_failOnChangeFlag)) { - final String? stateError = await _validateRepositoryState(); + final String? stateError = await _validateRepositoryState(package); if (stateError != null) { - printError('README.md is out of sync with its source excerpts.\n\n' - 'If you edited code in README.md directly, you should instead edit ' - 'the example source files. If you edited source files, run the ' - 'repository tooling\'s "$name" command on this package, and update ' - 'your PR with the resulting changes.'); + printError('One or more .md files are out of sync with their source ' + 'excerpts.\n\n' + 'If you edited code in a .md file directly, you should instead ' + 'edit the example source files. If you edited source files, run ' + 'the repository tooling\'s "$name" command on this package, and ' + 'update your PR with the resulting changes.\n\n' + 'For more information, see ' + 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#readme-code'); return PackageResult.fail([stateError]); } } @@ -138,14 +141,24 @@ class UpdateExcerptsCommand extends PackageLoopingCommand { return exitCode == 0; } - /// Runs the injection step to update [targetPackage]'s README with the latest - /// excerpts from [example], returning true on success. + /// Runs the injection step to update [targetPackage]'s top-level .md files + /// with the latest excerpts from [example], returning true on success. Future _injectSnippets( RepositoryPackage example, { required RepositoryPackage targetPackage, }) async { - final String relativeReadmePath = - getRelativePosixPath(targetPackage.readmeFile, from: example.directory); + final List relativeMdPaths = targetPackage.directory + .listSync() + .whereType() + .where((File f) => + f.basename.toLowerCase().endsWith('.md') && + // Exclude CHANGELOG since it should never have excerpts. + f.basename != 'CHANGELOG.md') + .map((File f) => getRelativePosixPath(f, from: example.directory)) + .toList(); + if (relativeMdPaths.isEmpty) { + return true; + } final int exitCode = await processRunner.runAndStream( 'dart', [ @@ -154,7 +167,7 @@ class UpdateExcerptsCommand extends PackageLoopingCommand { '--write-in-place', '--yaml', '--no-escape-ng-interpolation', - relativeReadmePath, + ...relativeMdPaths, ], workingDir: example.directory); return exitCode == 0; @@ -212,11 +225,11 @@ class UpdateExcerptsCommand extends PackageLoopingCommand { /// Checks the git state, returning an error string if any .md files have /// changed. - Future _validateRepositoryState() async { + Future _validateRepositoryState(RepositoryPackage package) async { final io.ProcessResult checkFiles = await processRunner.run( 'git', ['ls-files', '--modified'], - workingDir: packagesDir, + workingDir: package.directory, logOnError: true, ); if (checkFiles.exitCode != 0) { diff --git a/script/tool/test/update_excerpts_command_test.dart b/script/tool/test/update_excerpts_command_test.dart index 7bb0297de131..09862b3b3212 100644 --- a/script/tool/test/update_excerpts_command_test.dart +++ b/script/tool/test/update_excerpts_command_test.dart @@ -111,7 +111,7 @@ void main() { test('updates example readme when config is present', () async { final RepositoryPackage package = createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); + extraFiles: [kReadmeExcerptConfigPath, 'example/README.md']); final Directory example = getExampleDir(package); final List output = @@ -153,6 +153,52 @@ void main() { ])); }); + test('includes all top-level .md files', () async { + const String otherMdFileName = 'another_file.md'; + final RepositoryPackage package = createFakePlugin('a_package', packagesDir, + extraFiles: [kReadmeExcerptConfigPath, otherMdFileName]); + final Directory example = getExampleDir(package); + + final List output = + await runCapturingPrint(runner, ['update-excerpts']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall( + 'dart', + const [ + 'run', + 'build_runner', + 'build', + '--config', + 'excerpt', + '--output', + 'excerpts', + '--delete-conflicting-outputs', + ], + example.path), + ProcessCall( + 'dart', + const [ + 'run', + 'code_excerpt_updater', + '--write-in-place', + '--yaml', + '--no-escape-ng-interpolation', + '../README.md', + '../$otherMdFileName', + ], + example.path), + ])); + + expect( + output, + containsAllInOrder([ + contains('Ran for 1 package(s)'), + ])); + }); + test('skips when no config is present', () async { createFakePlugin('a_package', packagesDir); @@ -277,7 +323,7 @@ void main() { test('fails if example injection fails', () async { createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); + extraFiles: [kReadmeExcerptConfigPath, 'example/README.md']); processRunner.mockProcessesForExecutable['dart'] = [ FakeProcessInfo(MockProcess(), ['pub', 'get']), @@ -307,7 +353,7 @@ void main() { createFakePlugin('a_plugin', packagesDir, extraFiles: [kReadmeExcerptConfigPath]); - const String changedFilePath = 'packages/a_plugin/README.md'; + const String changedFilePath = 'README.md'; processRunner.mockProcessesForExecutable['git'] = [ FakeProcessInfo(MockProcess(stdout: changedFilePath)), ]; @@ -323,9 +369,10 @@ void main() { expect( output, containsAllInOrder([ - contains('README.md is out of sync with its source excerpts'), + contains( + 'One or more .md files are out of sync with their source excerpts'), contains('Snippets are out of sync in the following files: ' - 'packages/a_plugin/README.md'), + '$changedFilePath'), ])); }); From f27d3c93f2fb90e5a8d28cfa661405bc742467a9 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Thu, 15 Jun 2023 09:10:17 -0700 Subject: [PATCH 37/53] [go_router]Updates documentations around GoRouter.of, GoRouter.maybeOf, and BuildContext extension. (#4176) fixes https://github.com/flutter/flutter/issues/121506 --- packages/go_router/CHANGELOG.md | 5 +++++ packages/go_router/lib/src/misc/extensions.dart | 2 ++ packages/go_router/lib/src/router.dart | 4 ++++ packages/go_router/pubspec.yaml | 2 +- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 49764fbf9571..bb8d8d72027d 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,8 @@ +## 8.0.4 + +- Updates documentations around `GoRouter.of`, `GoRouter.maybeOf`, and `BuildContext` extension. + + ## 8.0.3 - Makes namedLocation and route name related APIs case sensitive. diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index c4ff4ad6a95a..58e3c3fea7c0 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -10,6 +10,8 @@ import '../router.dart'; /// context.go('/'); extension GoRouterHelper on BuildContext { /// Get a location from route name and parameters. + /// + /// This method can't be called during redirects. String namedLocation( String name, { Map pathParameters = const {}, diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index eac57c225581..15ceea634401 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -466,6 +466,8 @@ class GoRouter extends ChangeNotifier implements RouterConfig { } /// Find the current GoRouter in the widget tree. + /// + /// This method throws when it is called during redirects. static GoRouter of(BuildContext context) { final InheritedGoRouter? inherited = context.dependOnInheritedWidgetOfExactType(); @@ -474,6 +476,8 @@ class GoRouter extends ChangeNotifier implements RouterConfig { } /// The current GoRouter in the widget tree, if any. + /// + /// This method returns null when it is called during redirects. static GoRouter? maybeOf(BuildContext context) { final InheritedGoRouter? inherited = context.dependOnInheritedWidgetOfExactType(); diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index a2f48a79a434..390811da2d10 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 8.0.3 +version: 8.0.4 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 From cda21a89692739a3250091f061f9ac9d4bb8060e Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Thu, 15 Jun 2023 09:50:09 -0700 Subject: [PATCH 38/53] =?UTF-8?q?[go=5Frouter]=20Fixes=20bug=20that=20GoRo?= =?UTF-8?q?uterState=20in=20top=20level=20redirect=20doesn'=E2=80=A6=20(#4?= =?UTF-8?q?173)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …t contain complete data fixes https://github.com/flutter/flutter/issues/106461 --- packages/go_router/CHANGELOG.md | 5 ++- packages/go_router/lib/src/builder.dart | 25 ++++++------- packages/go_router/lib/src/configuration.dart | 3 +- packages/go_router/lib/src/state.dart | 28 +++++++++------ packages/go_router/pubspec.yaml | 2 +- packages/go_router/test/go_router_test.dart | 36 ++++++++++++++++++- 6 files changed, 70 insertions(+), 29 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index bb8d8d72027d..b0df45c5fe69 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,8 +1,11 @@ +## 8.0.5 + +- Fixes a bug that GoRouterState in top level redirect doesn't contain complete data. + ## 8.0.4 - Updates documentations around `GoRouter.of`, `GoRouter.maybeOf`, and `BuildContext` extension. - ## 8.0.3 - Makes namedLocation and route name related APIs case sensitive. diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart index d68958cbaa79..c98f2f8bd123 100644 --- a/packages/go_router/lib/src/builder.dart +++ b/packages/go_router/lib/src/builder.dart @@ -145,8 +145,7 @@ class RouteBuilder { if (matchList.isError) { keyToPage = , List>>{ navigatorKey: >[ - _buildErrorPage( - context, _buildErrorState(matchList.error!, matchList.uri)), + _buildErrorPage(context, _buildErrorState(matchList)), ] }; } else { @@ -325,8 +324,7 @@ class RouteBuilder { if (match is ImperativeRouteMatch) { effectiveMatchList = match.matches; if (effectiveMatchList.isError) { - return _buildErrorState( - effectiveMatchList.error!, effectiveMatchList.uri); + return _buildErrorState(effectiveMatchList); } } else { effectiveMatchList = matchList; @@ -491,19 +489,18 @@ class RouteBuilder { child: child, ); - GoRouterState _buildErrorState( - Exception error, - Uri uri, - ) { - final String location = uri.toString(); + GoRouterState _buildErrorState(RouteMatchList matchList) { + final String location = matchList.uri.toString(); + assert(matchList.isError); return GoRouterState( configuration, location: location, - matchedLocation: uri.path, - name: null, - queryParameters: uri.queryParameters, - queryParametersAll: uri.queryParametersAll, - error: error, + matchedLocation: matchList.uri.path, + fullPath: matchList.fullPath, + pathParameters: matchList.pathParameters, + queryParameters: matchList.uri.queryParameters, + queryParametersAll: matchList.uri.queryParametersAll, + error: matchList.error, pageKey: ValueKey('$location(error)'), ); } diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index 89fb5400de67..865007d76597 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -414,9 +414,10 @@ class RouteConfiguration { GoRouterState( this, location: prevLocation, - name: null, // No name available at the top level trim the query params off the // sub-location to match route.redirect + fullPath: prevMatchList.fullPath, + pathParameters: prevMatchList.pathParameters, matchedLocation: prevMatchList.uri.path, queryParameters: prevMatchList.uri.queryParameters, queryParametersAll: prevMatchList.uri.queryParametersAll, diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 9c634eddbeea..13ee9add91e2 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -17,12 +17,12 @@ class GoRouterState { this._configuration, { required this.location, required this.matchedLocation, - required this.name, + this.name, this.path, - this.fullPath, - this.pathParameters = const {}, - this.queryParameters = const {}, - this.queryParametersAll = const >{}, + required this.fullPath, + required this.pathParameters, + required this.queryParameters, + required this.queryParametersAll, this.extra, this.error, required this.pageKey, @@ -42,16 +42,24 @@ class GoRouterState { /// matchedLocation = /family/f2 final String matchedLocation; - /// The optional name of the route. + /// The optional name of the route associated with this app. + /// + /// This can be null for GoRouterState pass into top level redirect. final String? name; - /// The path to this sub-route, e.g. family/:fid + /// The path of the route associated with this app. e.g. family/:fid + /// + /// This can be null for GoRouterState pass into top level redirect. final String? path; /// The full path to this sub-route, e.g. /family/:fid + /// + /// For top level redirect, this is the entire path that matches the location. + /// It can be empty if go router can't find a match. In that case, the [error] + /// contains more information. final String? fullPath; - /// The parameters for this sub-route, e.g. {'fid': 'f2'} + /// The parameters for this match, e.g. {'fid': 'f2'} final Map pathParameters; /// The query parameters for the location, e.g. {'from': '/family/f2'} @@ -64,7 +72,7 @@ class GoRouterState { /// An extra object to pass along with the navigation. final Object? extra; - /// The error associated with this sub-route. + /// The error associated with this match. final Exception? error; /// A unique string key for this sub-route. @@ -129,8 +137,6 @@ class GoRouterState { /// Get a location from route name and parameters. /// This is useful for redirecting to a named location. - // TODO(chunhtai): remove this method when go_router can provide a way to - // look up named location during redirect. String namedLocation( String name, { Map pathParameters = const {}, diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index 390811da2d10..db489edff28b 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -1,7 +1,7 @@ name: go_router description: A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more -version: 8.0.4 +version: 8.0.5 repository: https://github.com/flutter/packages/tree/main/packages/go_router issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22 diff --git a/packages/go_router/test/go_router_test.dart b/packages/go_router/test/go_router_test.dart index ed258aa7e9b4..2ebb4afca774 100644 --- a/packages/go_router/test/go_router_test.dart +++ b/packages/go_router/test/go_router_test.dart @@ -2022,7 +2022,7 @@ void main() { expect(Uri.parse(state.location).queryParameters, isNotEmpty); expect(Uri.parse(state.matchedLocation).queryParameters, isEmpty); expect(state.path, isNull); - expect(state.fullPath, isNull); + expect(state.fullPath, '/login'); expect(state.pathParameters.length, 0); expect(state.queryParameters.length, 1); expect(state.queryParameters['from'], '/'); @@ -2036,6 +2036,40 @@ void main() { expect(find.byType(LoginScreen), findsOneWidget); }); + testWidgets('top-level redirect state contains path parameters', + (WidgetTester tester) async { + final List routes = [ + GoRoute( + path: '/', + builder: (BuildContext context, GoRouterState state) => + const DummyScreen(), + routes: [ + GoRoute( + path: ':id', + builder: (BuildContext context, GoRouterState state) => + const DummyScreen(), + ), + ]), + ]; + + final GoRouter router = await createRouter( + routes, + tester, + initialLocation: '/123', + redirect: (BuildContext context, GoRouterState state) { + expect(state.path, isNull); + expect(state.fullPath, '/:id'); + expect(state.pathParameters.length, 1); + expect(state.pathParameters['id'], '123'); + return null; + }, + ); + + final List matches = + router.routerDelegate.currentConfiguration.matches; + expect(matches, hasLength(2)); + }); + testWidgets('route-level redirect state', (WidgetTester tester) async { const String loc = '/book/0'; final List routes = [ From 94706c80da1d7c113b3790b0e9957aeef32d7efd Mon Sep 17 00:00:00 2001 From: gmackall <34871572+gmackall@users.noreply.github.com> Date: Thu, 15 Jun 2023 10:10:24 -0700 Subject: [PATCH 39/53] [camera_android] Upgrading roboelectric from 4.5 to 4.10.3 (#4018) https://github.com/flutter/flutter/issues/119752 Upgrades to allow configurations of sdk >= 31 (4.8.1 was chosen as it is the most common version used in other packages). --- packages/camera/camera_android/CHANGELOG.md | 1 + .../camera_android/android/build.gradle | 3 +- .../camera/media/MediaRecorderBuilder.java | 1 - .../resolution/ResolutionFeatureTest.java | 9 +++-- .../media/MediaRecorderBuilderTest.java | 36 ++++++++++++++++--- 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index 8bfdd7f0b1fe..c853b8a4b1be 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,6 +1,7 @@ ## NEXT * Fixes unawaited_futures violations. +* Removes duplicate line in `MediaRecorderBuilder.java`. ## 0.10.8+2 diff --git a/packages/camera/camera_android/android/build.gradle b/packages/camera/camera_android/android/build.gradle index ef8d9e297674..24a736e0e908 100644 --- a/packages/camera/camera_android/android/build.gradle +++ b/packages/camera/camera_android/android/build.gradle @@ -51,6 +51,7 @@ android { unitTests.includeAndroidResources = true unitTests.returnDefaultValues = true unitTests.all { + jvmArgs "-Xmx1g" testLogging { events "passed", "skipped", "failed", "standardOut", "standardError" outputs.upToDateWhen {false} @@ -65,5 +66,5 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'androidx.test:core:1.4.0' - testImplementation 'org.robolectric:robolectric:4.5' + testImplementation 'org.robolectric:robolectric:4.10.3' } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java index f55552edf898..966019bb1431 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -92,7 +92,6 @@ public MediaRecorder build() throws IOException, NullPointerException, IndexOutO mediaRecorder.setVideoEncodingBitRate(videoProfile.getBitrate()); mediaRecorder.setVideoFrameRate(videoProfile.getFrameRate()); mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); - mediaRecorder.setVideoSize(videoProfile.getWidth(), videoProfile.getHeight()); } else if (camcorderProfile != null) { mediaRecorder.setOutputFormat(camcorderProfile.fileFormat); if (enableAudio) { diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java index dbc352d697a4..fe4dcd795fed 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java @@ -91,8 +91,9 @@ public void beforeLegacy() { public void before() { mockProfileLow = mock(EncoderProfiles.class); EncoderProfiles mockProfile = mock(EncoderProfiles.class); - EncoderProfiles.VideoProfile mockVideoProfile = mock(EncoderProfiles.VideoProfile.class); - List mockVideoProfilesList = List.of(mockVideoProfile); + List mockVideoProfilesList = + new ArrayList(); + mockVideoProfilesList.add(null); mockedStaticProfile .when(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_HIGH)) @@ -117,8 +118,6 @@ public void before() { .thenReturn(mockProfileLow); when(mockProfile.getVideoProfiles()).thenReturn(mockVideoProfilesList); - when(mockVideoProfile.getHeight()).thenReturn(100); - when(mockVideoProfile.getWidth()).thenReturn(100); } @After @@ -386,7 +385,7 @@ public void computeBestPreviewSize_shouldUseLegacyBehaviorWhenEncoderProfilesNul @Config(minSdk = 31) @Test public void resolutionFeatureShouldUseLegacyBehaviorWhenEncoderProfilesNull() { - beforeLegacy(); + before(); try (MockedStatic mockedResolutionFeature = mockStatic(ResolutionFeature.class)) { mockedResolutionFeature diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java index 6cc58ee823d9..f37de01f5e7c 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -78,9 +78,9 @@ public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabledLegacy() throw public void build_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOException { EncoderProfiles recorderProfile = mock(EncoderProfiles.class); List mockVideoProfiles = - List.of(mock(EncoderProfiles.VideoProfile.class)); + List.of(getEmptyEncoderProfilesVideoProfile()); List mockAudioProfiles = - List.of(mock(EncoderProfiles.AudioProfile.class)); + List.of(getEmptyEncoderProfilesAudioProfile()); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); @@ -172,9 +172,9 @@ public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabledLegacy() throws public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOException { EncoderProfiles recorderProfile = mock(EncoderProfiles.class); List mockVideoProfiles = - List.of(mock(EncoderProfiles.VideoProfile.class)); + List.of(getEmptyEncoderProfilesVideoProfile()); List mockAudioProfiles = - List.of(mock(EncoderProfiles.AudioProfile.class)); + List.of(getEmptyEncoderProfilesAudioProfile()); MediaRecorderBuilder.MediaRecorderFactory mockFactory = mock(MediaRecorderBuilder.MediaRecorderFactory.class); MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); @@ -224,4 +224,32 @@ private CamcorderProfile getEmptyCamcorderProfile() { return null; } + + private EncoderProfiles.VideoProfile getEmptyEncoderProfilesVideoProfile() { + try { + Constructor constructor = + EncoderProfiles.VideoProfile.class.getDeclaredConstructor( + int.class, int.class, int.class, int.class, int.class, int.class); + + constructor.setAccessible(true); + return constructor.newInstance(0, 0, 0, 0, 0, 0); + } catch (Exception ignored) { + } + + return null; + } + + private EncoderProfiles.AudioProfile getEmptyEncoderProfilesAudioProfile() { + try { + Constructor constructor = + EncoderProfiles.AudioProfile.class.getDeclaredConstructor( + int.class, int.class, int.class, int.class, int.class); + + constructor.setAccessible(true); + return constructor.newInstance(0, 0, 0, 0, 0); + } catch (Exception ignored) { + } + + return null; + } } From 6a51bef71867add211b1b2d5be10f2bd3362b87d Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Thu, 15 Jun 2023 15:03:06 -0400 Subject: [PATCH 40/53] Roll Flutter from 95be76ab7e3d to b0188cd18274 (10 revisions) (#4221) https://github.com/flutter/flutter/compare/95be76ab7e3d...b0188cd18274 2023-06-15 mdebbar@google.com [web] Pass creation params to the platform view factory (flutter/flutter#128146) 2023-06-15 christopherfujino@gmail.com [flutter_tools] cache flutter sdk version to disk (flutter/flutter#124558) 2023-06-14 arne@molland.sh Fix inconsistently suffixed macOS flavored bundle directory (flutter/flutter#127997) 2023-06-14 36861262+QuncCccccc@users.noreply.github.com Update golden tests for material (flutter/flutter#128839) 2023-06-14 polinach@google.com Update getChildrenSummaryTree to handle Diagnosticable as input. (flutter/flutter#128833) 2023-06-14 ian@hixie.ch Improve the error message for non-normalized constraints (flutter/flutter#127906) 2023-06-14 ian@hixie.ch ContextAction.isEnabled needs a context (flutter/flutter#127721) 2023-06-14 louisehsu@google.com Remove temporary default case for PointerSignalKind (flutter/flutter#128900) 2023-06-14 polinach@google.com Respect allowlisted count of leaks. (flutter/flutter#128823) 2023-06-14 34871572+gmackall@users.noreply.github.com Unpin flutter_plugin_android_lifecycle (flutter/flutter#128898) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC rmistry@google.com,stuartmorgan@google.com,tarrinneal@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 0fe2e13c4e26..9c4319ac01a7 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -95be76ab7e3dca2def54454313e97f94f4ac4582 +b0188cd18274d44af9997440fa11fe29cbc504f6 From f185bffa21651c6339f78e89a65f8aea2aefbd95 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Thu, 15 Jun 2023 19:13:03 -0700 Subject: [PATCH 41/53] [url_launcher] Add ignores for deprecated member to test (#4220) We are going to deprecate `RendererBinding.renderView` for multi-view where the binding will manages multiple RenderViews. Change for that is staged in https://github.com/flutter/flutter/pull/125003. --- .../test/src/legacy_api_test.dart | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart index b2fde31d526d..e35312831755 100644 --- a/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart +++ b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart @@ -7,6 +7,7 @@ import 'dart:ui' show Brightness; import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart' show PlatformException; import 'package:flutter_test/flutter_test.dart'; import 'package:url_launcher/src/legacy_api.dart'; @@ -241,15 +242,18 @@ void main() { _anonymize(TestWidgetsFlutterBinding.ensureInitialized())! as TestWidgetsFlutterBinding; debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - binding.renderView.automaticSystemUiAdjustment = true; + // TODO(goderbauer): Migrate to binding.renderViews when that is available in the oldest supported stable. + final RenderView renderView = + binding.renderView; // ignore: deprecated_member_use + renderView.automaticSystemUiAdjustment = true; final Future launchResult = launch('http://flutter.dev/', statusBarBrightness: Brightness.dark); // Should take over control of the automaticSystemUiAdjustment while it's // pending, then restore it back to normal after the launch finishes. - expect(binding.renderView.automaticSystemUiAdjustment, isFalse); + expect(renderView.automaticSystemUiAdjustment, isFalse); await launchResult; - expect(binding.renderView.automaticSystemUiAdjustment, isTrue); + expect(renderView.automaticSystemUiAdjustment, isTrue); }); test('sets automaticSystemUiAdjustment to not be null', () async { @@ -270,15 +274,18 @@ void main() { _anonymize(TestWidgetsFlutterBinding.ensureInitialized())! as TestWidgetsFlutterBinding; debugDefaultTargetPlatformOverride = TargetPlatform.android; - expect(binding.renderView.automaticSystemUiAdjustment, true); + // TODO(goderbauer): Migrate to binding.renderViews when that is available in the oldest supported stable. + final RenderView renderView = + binding.renderView; // ignore: deprecated_member_use + expect(renderView.automaticSystemUiAdjustment, true); final Future launchResult = launch('http://flutter.dev/', statusBarBrightness: Brightness.dark); // The automaticSystemUiAdjustment should be set before the launch // and equal to true after the launch result is complete. - expect(binding.renderView.automaticSystemUiAdjustment, true); + expect(renderView.automaticSystemUiAdjustment, true); await launchResult; - expect(binding.renderView.automaticSystemUiAdjustment, true); + expect(renderView.automaticSystemUiAdjustment, true); }); test('open non-parseable url', () async { From 754405dc0707e0181c700947b4595f7f54b62440 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 16 Jun 2023 08:43:21 -0400 Subject: [PATCH 42/53] [google_map_flutter] Fix map object regression due to async changes (#4171) In #4067 I changed several implicitly unawaited futures to `await`, not noticing that doing so would change the timing on updating fields that were used to compute diffs for future updates. Since the update flow is unawaited at higher levels, this meant that multiple calls to update map objects in rapid succession would compute incorrect diffs due to using the wrong base objects. This fixes that by restoring the old flow using `unawaited`. Adds new tests that introduce synthetic awaits into the fake platform interface object and perform multiple updates in a row that should have different base object sets, which fail without the fix. In order to simplify adding that behavior to the fake, and to pay down technical debt in the unit tests, I replaced the old test fake that is based on method channel interception (which predates the federation of the plugin and is implicitly relying on the method channel implementation from a different package never changing) with a fake platform interface implementation (substantially simplifying the fake). Fixes https://github.com/flutter/flutter/issues/128042 --- .../google_maps_flutter/CHANGELOG.md | 4 +- .../lib/src/google_map.dart | 20 +- .../google_maps_flutter/pubspec.yaml | 2 +- .../test/circle_updates_test.dart | 152 +++--- .../fake_google_maps_flutter_platform.dart | 303 +++++++++++ .../test/fake_maps_controllers.dart | 485 ------------------ .../test/google_map_test.dart | 160 +++--- .../test/map_creation_test.dart | 229 +-------- .../test/marker_updates_test.dart | 153 +++--- .../test/polygon_updates_test.dart | 252 ++++----- .../test/polyline_updates_test.dart | 170 +++--- .../test/tile_overlay_updates_test.dart | 161 +----- .../example/pubspec.yaml | 7 + 13 files changed, 783 insertions(+), 1315 deletions(-) create mode 100644 packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart delete mode 100644 packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index e244734a438e..6edda3e08049 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT +## 2.3.1 +* Fixes a regression from 2.2.8 that could cause incorrect handling of a + rapid series of map object updates. * Fixes stale ignore: prefer_const_constructors. * Updates minimum supported SDK version to Flutter 3.10/Dart 3.0. diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index e1b710c307a6..08c2286527fb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -360,41 +360,41 @@ class _GoogleMapState extends State { return; } final GoogleMapController controller = await _controller.future; - await controller._updateMapConfiguration(updates); + unawaited(controller._updateMapConfiguration(updates)); _mapConfiguration = newConfig; } Future _updateMarkers() async { final GoogleMapController controller = await _controller.future; - await controller._updateMarkers( - MarkerUpdates.from(_markers.values.toSet(), widget.markers)); + unawaited(controller._updateMarkers( + MarkerUpdates.from(_markers.values.toSet(), widget.markers))); _markers = keyByMarkerId(widget.markers); } Future _updatePolygons() async { final GoogleMapController controller = await _controller.future; - await controller._updatePolygons( - PolygonUpdates.from(_polygons.values.toSet(), widget.polygons)); + unawaited(controller._updatePolygons( + PolygonUpdates.from(_polygons.values.toSet(), widget.polygons))); _polygons = keyByPolygonId(widget.polygons); } Future _updatePolylines() async { final GoogleMapController controller = await _controller.future; - await controller._updatePolylines( - PolylineUpdates.from(_polylines.values.toSet(), widget.polylines)); + unawaited(controller._updatePolylines( + PolylineUpdates.from(_polylines.values.toSet(), widget.polylines))); _polylines = keyByPolylineId(widget.polylines); } Future _updateCircles() async { final GoogleMapController controller = await _controller.future; - await controller._updateCircles( - CircleUpdates.from(_circles.values.toSet(), widget.circles)); + unawaited(controller._updateCircles( + CircleUpdates.from(_circles.values.toSet(), widget.circles))); _circles = keyByCircleId(widget.circles); } Future _updateTileOverlays() async { final GoogleMapController controller = await _controller.future; - await controller._updateTileOverlays(widget.tileOverlays); + unawaited(controller._updateTileOverlays(widget.tileOverlays)); } Future onPlatformViewCreated(int id) async { diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 91bc476fda86..e56ff513e7a4 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. repository: https://github.com/flutter/packages/tree/main/packages/google_maps_flutter/google_maps_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.3.0 +version: 2.3.1 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart index 459e16b60c42..f94caf1f5837 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'fake_maps_controllers.dart'; +import 'fake_google_maps_flutter_platform.dart'; Widget _mapWithCircles(Set circles) { return Directionality( @@ -20,36 +20,24 @@ Widget _mapWithCircles(Set circles) { } void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - }); + late FakeGoogleMapsFlutterPlatform platform; setUp(() { - fakePlatformViewsController.reset(); + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; }); testWidgets('Initializing a circle', (WidgetTester tester) async { const Circle c1 = Circle(circleId: CircleId('circle_1')); await tester.pumpWidget(_mapWithCircles({c1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.circlesToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.circleUpdates.last.circlesToAdd.length, 1); - final Circle initializedCircle = platformGoogleMap.circlesToAdd.first; + final Circle initializedCircle = map.circleUpdates.last.circlesToAdd.first; expect(initializedCircle, equals(c1)); - expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); - expect(platformGoogleMap.circlesToChange.isEmpty, true); + expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true); + expect(map.circleUpdates.last.circlesToChange.isEmpty, true); }); testWidgets('Adding a circle', (WidgetTester tester) async { @@ -59,16 +47,15 @@ void main() { await tester.pumpWidget(_mapWithCircles({c1})); await tester.pumpWidget(_mapWithCircles({c1, c2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.circlesToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.circleUpdates.last.circlesToAdd.length, 1); - final Circle addedCircle = platformGoogleMap.circlesToAdd.first; + final Circle addedCircle = map.circleUpdates.last.circlesToAdd.first; expect(addedCircle, equals(c2)); - expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); + expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true); - expect(platformGoogleMap.circlesToChange.isEmpty, true); + expect(map.circleUpdates.last.circlesToChange.isEmpty, true); }); testWidgets('Removing a circle', (WidgetTester tester) async { @@ -77,13 +64,12 @@ void main() { await tester.pumpWidget(_mapWithCircles({c1})); await tester.pumpWidget(_mapWithCircles({})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.circleIdsToRemove.length, 1); - expect(platformGoogleMap.circleIdsToRemove.first, equals(c1.circleId)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.circleUpdates.last.circleIdsToRemove.length, 1); + expect(map.circleUpdates.last.circleIdsToRemove.first, equals(c1.circleId)); - expect(platformGoogleMap.circlesToChange.isEmpty, true); - expect(platformGoogleMap.circlesToAdd.isEmpty, true); + expect(map.circleUpdates.last.circlesToChange.isEmpty, true); + expect(map.circleUpdates.last.circlesToAdd.isEmpty, true); }); testWidgets('Updating a circle', (WidgetTester tester) async { @@ -93,13 +79,12 @@ void main() { await tester.pumpWidget(_mapWithCircles({c1})); await tester.pumpWidget(_mapWithCircles({c2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.circlesToChange.length, 1); - expect(platformGoogleMap.circlesToChange.first, equals(c2)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.circleUpdates.last.circlesToChange.length, 1); + expect(map.circleUpdates.last.circlesToChange.first, equals(c2)); - expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); - expect(platformGoogleMap.circlesToAdd.isEmpty, true); + expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true); + expect(map.circleUpdates.last.circlesToAdd.isEmpty, true); }); testWidgets('Updating a circle', (WidgetTester tester) async { @@ -109,11 +94,10 @@ void main() { await tester.pumpWidget(_mapWithCircles({c1})); await tester.pumpWidget(_mapWithCircles({c2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.circlesToChange.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.circleUpdates.last.circlesToChange.length, 1); - final Circle update = platformGoogleMap.circlesToChange.first; + final Circle update = map.circleUpdates.last.circlesToChange.first; expect(update, equals(c2)); expect(update.radius, 10); }); @@ -129,12 +113,11 @@ void main() { await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.circlesToChange, cur); - expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); - expect(platformGoogleMap.circlesToAdd.isEmpty, true); + expect(map.circleUpdates.last.circlesToChange, cur); + expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true); + expect(map.circleUpdates.last.circlesToAdd.isEmpty, true); }); testWidgets('Multi Update', (WidgetTester tester) async { @@ -150,16 +133,15 @@ void main() { await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.circlesToChange.length, 1); - expect(platformGoogleMap.circlesToAdd.length, 1); - expect(platformGoogleMap.circleIdsToRemove.length, 1); + expect(map.circleUpdates.last.circlesToChange.length, 1); + expect(map.circleUpdates.last.circlesToAdd.length, 1); + expect(map.circleUpdates.last.circleIdsToRemove.length, 1); - expect(platformGoogleMap.circlesToChange.first, equals(c2)); - expect(platformGoogleMap.circlesToAdd.first, equals(c1)); - expect(platformGoogleMap.circleIdsToRemove.first, equals(c3.circleId)); + expect(map.circleUpdates.last.circlesToChange.first, equals(c2)); + expect(map.circleUpdates.last.circlesToAdd.first, equals(c1)); + expect(map.circleUpdates.last.circleIdsToRemove.first, equals(c3.circleId)); }); testWidgets('Partial Update', (WidgetTester tester) async { @@ -173,12 +155,11 @@ void main() { await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.circlesToChange, {c3}); - expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); - expect(platformGoogleMap.circlesToAdd.isEmpty, true); + expect(map.circleUpdates.last.circlesToChange, {c3}); + expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true); + expect(map.circleUpdates.last.circlesToAdd.isEmpty, true); }); testWidgets('Update non platform related attr', (WidgetTester tester) async { @@ -190,17 +171,42 @@ void main() { await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.circlesToChange.isEmpty, true); - expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); - expect(platformGoogleMap.circlesToAdd.isEmpty, true); + expect(map.circleUpdates.last.circlesToChange.isEmpty, true); + expect(map.circleUpdates.last.circleIdsToRemove.isEmpty, true); + expect(map.circleUpdates.last.circlesToAdd.isEmpty, true); }); -} -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; + testWidgets('multi-update with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Circle c1 = Circle(circleId: CircleId('circle_1')); + const Circle c2 = Circle(circleId: CircleId('circle_2')); + const Circle c3 = Circle(circleId: CircleId('circle_3'), radius: 1); + const Circle c3updated = Circle(circleId: CircleId('circle_3'), radius: 10); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithCircles({c1, c2})); + await tester.pumpWidget(_mapWithCircles({c1, c3})); + await tester.pumpWidget(_mapWithCircles({c1, c3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.circleUpdates.length, 3); + + expect(map.circleUpdates[0].circlesToChange.isEmpty, true); + expect(map.circleUpdates[0].circlesToAdd, {c1, c2}); + expect(map.circleUpdates[0].circleIdsToRemove.isEmpty, true); + + expect(map.circleUpdates[1].circlesToChange.isEmpty, true); + expect(map.circleUpdates[1].circlesToAdd, {c3}); + expect(map.circleUpdates[1].circleIdsToRemove, {c2.circleId}); + + expect(map.circleUpdates[2].circlesToChange, {c3updated}); + expect(map.circleUpdates[2].circlesToAdd.isEmpty, true); + expect(map.circleUpdates[2].circleIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart new file mode 100644 index 000000000000..22447ba5ecad --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/test/fake_google_maps_flutter_platform.dart @@ -0,0 +1,303 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:stream_transform/stream_transform.dart'; + +// A dummy implementation of the platform interface for tests. +class FakeGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { + FakeGoogleMapsFlutterPlatform(); + + /// The IDs passed to each call to buildView, in call order. + List createdIds = []; + + /// A map of creation IDs to fake map instances. + Map mapInstances = + {}; + + PlatformMapStateRecorder get lastCreatedMap => mapInstances[createdIds.last]!; + + /// Whether to add a small delay to async calls to simulate more realistic + /// async behavior (simulating the platform channel calls most + /// implementations will do). + /// + /// When true, requires tests to `pumpAndSettle` at the end of the test + /// to avoid exceptions. + bool simulatePlatformDelay = false; + + /// Whether `dispose` has been called. + bool disposed = false; + + /// Stream controller to inject events for testing. + final StreamController> mapEventStreamController = + StreamController>.broadcast(); + + @override + Future init(int mapId) async {} + + @override + Future updateMapConfiguration( + MapConfiguration update, { + required int mapId, + }) async { + mapInstances[mapId]?.mapConfiguration = update; + await _fakeDelay(); + } + + @override + Future updateMarkers( + MarkerUpdates markerUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.markerUpdates.add(markerUpdates); + await _fakeDelay(); + } + + @override + Future updatePolygons( + PolygonUpdates polygonUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.polygonUpdates.add(polygonUpdates); + await _fakeDelay(); + } + + @override + Future updatePolylines( + PolylineUpdates polylineUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.polylineUpdates.add(polylineUpdates); + await _fakeDelay(); + } + + @override + Future updateCircles( + CircleUpdates circleUpdates, { + required int mapId, + }) async { + mapInstances[mapId]?.circleUpdates.add(circleUpdates); + await _fakeDelay(); + } + + @override + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) async { + mapInstances[mapId]?.tileOverlaySets.add(newTileOverlays); + await _fakeDelay(); + } + + @override + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) async {} + + @override + Future animateCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async {} + + @override + Future moveCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async {} + + @override + Future setMapStyle( + String? mapStyle, { + required int mapId, + }) async {} + + @override + Future getVisibleRegion({ + required int mapId, + }) async { + return LatLngBounds( + southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)); + } + + @override + Future getScreenCoordinate( + LatLng latLng, { + required int mapId, + }) async { + return const ScreenCoordinate(x: 0, y: 0); + } + + @override + Future getLatLng( + ScreenCoordinate screenCoordinate, { + required int mapId, + }) async { + return const LatLng(0, 0); + } + + @override + Future showMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async {} + + @override + Future hideMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async {} + + @override + Future isMarkerInfoWindowShown( + MarkerId markerId, { + required int mapId, + }) async { + return false; + } + + @override + Future getZoomLevel({ + required int mapId, + }) async { + return 0.0; + } + + @override + Future takeSnapshot({ + required int mapId, + }) async { + return null; + } + + @override + Stream onCameraMoveStarted({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCameraMove({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCameraIdle({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onInfoWindowTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDragStart({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDragEnd({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onPolylineTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onPolygonTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCircleTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onLongPress({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + void dispose({required int mapId}) { + disposed = true; + } + + @override + Widget buildViewWithConfiguration( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required MapWidgetConfiguration widgetConfiguration, + MapObjects mapObjects = const MapObjects(), + MapConfiguration mapConfiguration = const MapConfiguration(), + }) { + final PlatformMapStateRecorder? instance = mapInstances[creationId]; + if (instance == null) { + createdIds.add(creationId); + mapInstances[creationId] = PlatformMapStateRecorder( + widgetConfiguration: widgetConfiguration, + mapConfiguration: mapConfiguration, + mapObjects: mapObjects); + onPlatformViewCreated(creationId); + } + return Container(); + } + + Future _fakeDelay() async { + if (!simulatePlatformDelay) { + return; + } + return Future.delayed(const Duration(microseconds: 1)); + } +} + +/// A fake implementation of a native map, which stores all the updates it is +/// sent for inspection in tests. +class PlatformMapStateRecorder { + PlatformMapStateRecorder({ + required this.widgetConfiguration, + this.mapObjects = const MapObjects(), + this.mapConfiguration = const MapConfiguration(), + }) { + markerUpdates.add(MarkerUpdates.from(const {}, mapObjects.markers)); + polygonUpdates + .add(PolygonUpdates.from(const {}, mapObjects.polygons)); + polylineUpdates + .add(PolylineUpdates.from(const {}, mapObjects.polylines)); + circleUpdates.add(CircleUpdates.from(const {}, mapObjects.circles)); + tileOverlaySets.add(mapObjects.tileOverlays); + } + + MapWidgetConfiguration widgetConfiguration; + MapObjects mapObjects; + MapConfiguration mapConfiguration; + + final List markerUpdates = []; + final List polygonUpdates = []; + final List polylineUpdates = []; + final List circleUpdates = []; + final List> tileOverlaySets = >[]; +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart deleted file mode 100644 index c28ff1f4f55f..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart +++ /dev/null @@ -1,485 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:typed_data'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; - -class FakePlatformGoogleMap { - FakePlatformGoogleMap(int id, Map params) - : cameraPosition = - CameraPosition.fromMap(params['initialCameraPosition']), - channel = MethodChannel('plugins.flutter.io/google_maps_$id') { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler(channel, onMethodCall); - updateOptions(params['options'] as Map); - updateMarkers(params); - updatePolygons(params); - updatePolylines(params); - updateCircles(params); - updateTileOverlays(Map.castFrom(params)); - } - - MethodChannel channel; - - CameraPosition? cameraPosition; - - bool? compassEnabled; - - bool? mapToolbarEnabled; - - CameraTargetBounds? cameraTargetBounds; - - MapType? mapType; - - MinMaxZoomPreference? minMaxZoomPreference; - - bool? rotateGesturesEnabled; - - bool? scrollGesturesEnabled; - - bool? tiltGesturesEnabled; - - bool? zoomGesturesEnabled; - - bool? zoomControlsEnabled; - - bool? liteModeEnabled; - - bool? trackCameraPosition; - - bool? myLocationEnabled; - - bool? trafficEnabled; - - bool? buildingsEnabled; - - bool? myLocationButtonEnabled; - - List? padding; - - Set markerIdsToRemove = {}; - - Set markersToAdd = {}; - - Set markersToChange = {}; - - Set polygonIdsToRemove = {}; - - Set polygonsToAdd = {}; - - Set polygonsToChange = {}; - - Set polylineIdsToRemove = {}; - - Set polylinesToAdd = {}; - - Set polylinesToChange = {}; - - Set circleIdsToRemove = {}; - - Set circlesToAdd = {}; - - Set circlesToChange = {}; - - Set tileOverlayIdsToRemove = {}; - - Set tileOverlaysToAdd = {}; - - Set tileOverlaysToChange = {}; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'map#update': - final Map arguments = - (call.arguments as Map).cast(); - updateOptions(arguments['options']! as Map); - return Future.sync(() {}); - case 'markers#update': - updateMarkers(call.arguments as Map?); - return Future.sync(() {}); - case 'polygons#update': - updatePolygons(call.arguments as Map?); - return Future.sync(() {}); - case 'polylines#update': - updatePolylines(call.arguments as Map?); - return Future.sync(() {}); - case 'tileOverlays#update': - updateTileOverlays(Map.castFrom( - call.arguments as Map)); - return Future.sync(() {}); - case 'circles#update': - updateCircles(call.arguments as Map?); - return Future.sync(() {}); - default: - return Future.sync(() {}); - } - } - - void updateMarkers(Map? markerUpdates) { - if (markerUpdates == null) { - return; - } - markersToAdd = _deserializeMarkers(markerUpdates['markersToAdd']); - markerIdsToRemove = _deserializeMarkerIds( - markerUpdates['markerIdsToRemove'] as List?); - markersToChange = _deserializeMarkers(markerUpdates['markersToChange']); - } - - Set _deserializeMarkerIds(List? markerIds) { - if (markerIds == null) { - return {}; - } - return markerIds - .map((dynamic markerId) => MarkerId(markerId as String)) - .toSet(); - } - - Set _deserializeMarkers(dynamic markers) { - if (markers == null) { - return {}; - } - final List markersData = markers as List; - final Set result = {}; - for (final Map markerData - in markersData.cast>()) { - final String markerId = markerData['markerId'] as String; - final double alpha = markerData['alpha'] as double; - final bool draggable = markerData['draggable'] as bool; - final bool visible = markerData['visible'] as bool; - - final dynamic infoWindowData = markerData['infoWindow']; - InfoWindow infoWindow = InfoWindow.noText; - if (infoWindowData != null) { - final Map infoWindowMap = - infoWindowData as Map; - infoWindow = InfoWindow( - title: infoWindowMap['title'] as String?, - snippet: infoWindowMap['snippet'] as String?, - ); - } - - result.add(Marker( - markerId: MarkerId(markerId), - draggable: draggable, - visible: visible, - infoWindow: infoWindow, - alpha: alpha, - )); - } - - return result; - } - - void updatePolygons(Map? polygonUpdates) { - if (polygonUpdates == null) { - return; - } - polygonsToAdd = _deserializePolygons(polygonUpdates['polygonsToAdd']); - polygonIdsToRemove = _deserializePolygonIds( - polygonUpdates['polygonIdsToRemove'] as List?); - polygonsToChange = _deserializePolygons(polygonUpdates['polygonsToChange']); - } - - Set _deserializePolygonIds(List? polygonIds) { - if (polygonIds == null) { - return {}; - } - return polygonIds - .map((dynamic polygonId) => PolygonId(polygonId as String)) - .toSet(); - } - - Set _deserializePolygons(dynamic polygons) { - if (polygons == null) { - return {}; - } - final List polygonsData = polygons as List; - final Set result = {}; - for (final Map polygonData - in polygonsData.cast>()) { - final String polygonId = polygonData['polygonId'] as String; - final bool visible = polygonData['visible'] as bool; - final bool geodesic = polygonData['geodesic'] as bool; - final List points = - _deserializePoints(polygonData['points'] as List); - final List> holes = - _deserializeHoles(polygonData['holes'] as List); - - result.add(Polygon( - polygonId: PolygonId(polygonId), - visible: visible, - geodesic: geodesic, - points: points, - holes: holes, - )); - } - - return result; - } - - // Converts a list of points expressed as two-element lists of doubles into - // a list of `LatLng`s. All list items are assumed to be non-null. - List _deserializePoints(List points) { - return points.map((dynamic item) { - final List list = item as List; - return LatLng(list[0]! as double, list[1]! as double); - }).toList(); - } - - List> _deserializeHoles(List holes) { - return holes.map>((dynamic hole) { - return _deserializePoints(hole as List); - }).toList(); - } - - void updatePolylines(Map? polylineUpdates) { - if (polylineUpdates == null) { - return; - } - polylinesToAdd = _deserializePolylines(polylineUpdates['polylinesToAdd']); - polylineIdsToRemove = _deserializePolylineIds( - polylineUpdates['polylineIdsToRemove'] as List?); - polylinesToChange = - _deserializePolylines(polylineUpdates['polylinesToChange']); - } - - Set _deserializePolylineIds(List? polylineIds) { - if (polylineIds == null) { - return {}; - } - return polylineIds - .map((dynamic polylineId) => PolylineId(polylineId as String)) - .toSet(); - } - - Set _deserializePolylines(dynamic polylines) { - if (polylines == null) { - return {}; - } - final List polylinesData = polylines as List; - final Set result = {}; - for (final Map polylineData - in polylinesData.cast>()) { - final String polylineId = polylineData['polylineId'] as String; - final bool visible = polylineData['visible'] as bool; - final bool geodesic = polylineData['geodesic'] as bool; - final List points = - _deserializePoints(polylineData['points'] as List); - - result.add(Polyline( - polylineId: PolylineId(polylineId), - visible: visible, - geodesic: geodesic, - points: points, - )); - } - - return result; - } - - void updateCircles(Map? circleUpdates) { - if (circleUpdates == null) { - return; - } - circlesToAdd = _deserializeCircles(circleUpdates['circlesToAdd']); - circleIdsToRemove = _deserializeCircleIds( - circleUpdates['circleIdsToRemove'] as List?); - circlesToChange = _deserializeCircles(circleUpdates['circlesToChange']); - } - - void updateTileOverlays(Map updateTileOverlayUpdates) { - final List>? tileOverlaysToAddList = - updateTileOverlayUpdates['tileOverlaysToAdd'] != null - ? List.castFrom>( - updateTileOverlayUpdates['tileOverlaysToAdd'] as List) - : null; - final List? tileOverlayIdsToRemoveList = - updateTileOverlayUpdates['tileOverlayIdsToRemove'] != null - ? List.castFrom( - updateTileOverlayUpdates['tileOverlayIdsToRemove'] - as List) - : null; - final List>? tileOverlaysToChangeList = - updateTileOverlayUpdates['tileOverlaysToChange'] != null - ? List.castFrom>( - updateTileOverlayUpdates['tileOverlaysToChange'] - as List) - : null; - tileOverlaysToAdd = _deserializeTileOverlays(tileOverlaysToAddList); - tileOverlayIdsToRemove = - _deserializeTileOverlayIds(tileOverlayIdsToRemoveList); - tileOverlaysToChange = _deserializeTileOverlays(tileOverlaysToChangeList); - } - - Set _deserializeCircleIds(List? circleIds) { - if (circleIds == null) { - return {}; - } - return circleIds - .map((dynamic circleId) => CircleId(circleId as String)) - .toSet(); - } - - Set _deserializeCircles(dynamic circles) { - if (circles == null) { - return {}; - } - final List circlesData = circles as List; - final Set result = {}; - for (final Map circleData - in circlesData.cast>()) { - final String circleId = circleData['circleId'] as String; - final bool visible = circleData['visible'] as bool; - final double radius = circleData['radius'] as double; - - result.add(Circle( - circleId: CircleId(circleId), - visible: visible, - radius: radius, - )); - } - - return result; - } - - Set _deserializeTileOverlayIds(List? tileOverlayIds) { - if (tileOverlayIds == null || tileOverlayIds.isEmpty) { - return {}; - } - return tileOverlayIds - .map((String tileOverlayId) => TileOverlayId(tileOverlayId)) - .toSet(); - } - - Set _deserializeTileOverlays( - List>? tileOverlays) { - if (tileOverlays == null || tileOverlays.isEmpty) { - return {}; - } - final Set result = {}; - for (final Map tileOverlayData in tileOverlays) { - final String tileOverlayId = tileOverlayData['tileOverlayId'] as String; - final bool fadeIn = tileOverlayData['fadeIn'] as bool; - final double transparency = tileOverlayData['transparency'] as double; - final int zIndex = tileOverlayData['zIndex'] as int; - final bool visible = tileOverlayData['visible'] as bool; - - result.add(TileOverlay( - tileOverlayId: TileOverlayId(tileOverlayId), - fadeIn: fadeIn, - transparency: transparency, - zIndex: zIndex, - visible: visible, - )); - } - - return result; - } - - void updateOptions(Map options) { - if (options.containsKey('compassEnabled')) { - compassEnabled = options['compassEnabled'] as bool?; - } - if (options.containsKey('mapToolbarEnabled')) { - mapToolbarEnabled = options['mapToolbarEnabled'] as bool?; - } - if (options.containsKey('cameraTargetBounds')) { - final List boundsList = - options['cameraTargetBounds'] as List; - cameraTargetBounds = boundsList[0] == null - ? CameraTargetBounds.unbounded - : CameraTargetBounds(LatLngBounds.fromList(boundsList[0])); - } - if (options.containsKey('mapType')) { - mapType = MapType.values[options['mapType'] as int]; - } - if (options.containsKey('minMaxZoomPreference')) { - final List minMaxZoomList = - options['minMaxZoomPreference'] as List; - minMaxZoomPreference = MinMaxZoomPreference( - minMaxZoomList[0] as double?, minMaxZoomList[1] as double?); - } - if (options.containsKey('rotateGesturesEnabled')) { - rotateGesturesEnabled = options['rotateGesturesEnabled'] as bool?; - } - if (options.containsKey('scrollGesturesEnabled')) { - scrollGesturesEnabled = options['scrollGesturesEnabled'] as bool?; - } - if (options.containsKey('tiltGesturesEnabled')) { - tiltGesturesEnabled = options['tiltGesturesEnabled'] as bool?; - } - if (options.containsKey('trackCameraPosition')) { - trackCameraPosition = options['trackCameraPosition'] as bool?; - } - if (options.containsKey('zoomGesturesEnabled')) { - zoomGesturesEnabled = options['zoomGesturesEnabled'] as bool?; - } - if (options.containsKey('zoomControlsEnabled')) { - zoomControlsEnabled = options['zoomControlsEnabled'] as bool?; - } - if (options.containsKey('liteModeEnabled')) { - liteModeEnabled = options['liteModeEnabled'] as bool?; - } - if (options.containsKey('myLocationEnabled')) { - myLocationEnabled = options['myLocationEnabled'] as bool?; - } - if (options.containsKey('myLocationButtonEnabled')) { - myLocationButtonEnabled = options['myLocationButtonEnabled'] as bool?; - } - if (options.containsKey('trafficEnabled')) { - trafficEnabled = options['trafficEnabled'] as bool?; - } - if (options.containsKey('buildingsEnabled')) { - buildingsEnabled = options['buildingsEnabled'] as bool?; - } - if (options.containsKey('padding')) { - padding = options['padding'] as List?; - } - } -} - -class FakePlatformViewsController { - FakePlatformGoogleMap? lastCreatedView; - - Future fakePlatformViewsMethodHandler(MethodCall call) { - switch (call.method) { - case 'create': - final Map args = - call.arguments as Map; - final Map params = - _decodeParams(args['params'] as Uint8List)!; - lastCreatedView = FakePlatformGoogleMap( - args['id'] as int, - params, - ); - return Future.sync(() => 1); - default: - return Future.sync(() {}); - } - } - - void reset() { - lastCreatedView = null; - } -} - -Map? _decodeParams(Uint8List paramsMessage) { - final ByteBuffer buffer = paramsMessage.buffer; - final ByteData messageBytes = buffer.asByteData( - paramsMessage.offsetInBytes, - paramsMessage.lengthInBytes, - ); - return const StandardMessageCodec().decodeMessage(messageBytes) - as Map?; -} - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart index 99b12988f3b4..7005a8d3ab60 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart @@ -2,30 +2,19 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'fake_maps_controllers.dart'; +import 'fake_google_maps_flutter_platform.dart'; void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - }); + late FakeGoogleMapsFlutterPlatform platform; setUp(() { - fakePlatformViewsController.reset(); + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; }); testWidgets('Initial camera position', (WidgetTester tester) async { @@ -38,10 +27,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.cameraPosition, + expect(map.widgetConfiguration.initialCameraPosition, const CameraPosition(target: LatLng(10.0, 15.0))); }); @@ -65,10 +53,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.cameraPosition, + expect(map.widgetConfiguration.initialCameraPosition, const CameraPosition(target: LatLng(10.0, 15.0))); }); @@ -83,10 +70,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.compassEnabled, false); + expect(map.mapConfiguration.compassEnabled, false); await tester.pumpWidget( const Directionality( @@ -97,7 +83,7 @@ void main() { ), ); - expect(platformGoogleMap.compassEnabled, true); + expect(map.mapConfiguration.compassEnabled, true); }); testWidgets('Can update mapToolbarEnabled', (WidgetTester tester) async { @@ -111,10 +97,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.mapToolbarEnabled, false); + expect(map.mapConfiguration.mapToolbarEnabled, false); await tester.pumpWidget( const Directionality( @@ -125,7 +110,7 @@ void main() { ), ); - expect(platformGoogleMap.mapToolbarEnabled, true); + expect(map.mapConfiguration.mapToolbarEnabled, true); }); testWidgets('Can update cameraTargetBounds', (WidgetTester tester) async { @@ -145,11 +130,10 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; expect( - platformGoogleMap.cameraTargetBounds, + map.mapConfiguration.cameraTargetBounds, CameraTargetBounds( LatLngBounds( southwest: const LatLng(10.0, 20.0), @@ -174,7 +158,7 @@ void main() { ); expect( - platformGoogleMap.cameraTargetBounds, + map.mapConfiguration.cameraTargetBounds, CameraTargetBounds( LatLngBounds( southwest: const LatLng(16.0, 20.0), @@ -194,10 +178,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.mapType, MapType.hybrid); + expect(map.mapConfiguration.mapType, MapType.hybrid); await tester.pumpWidget( const Directionality( @@ -209,7 +192,7 @@ void main() { ), ); - expect(platformGoogleMap.mapType, MapType.satellite); + expect(map.mapConfiguration.mapType, MapType.satellite); }); testWidgets('Can update minMaxZoom', (WidgetTester tester) async { @@ -223,10 +206,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.minMaxZoomPreference, + expect(map.mapConfiguration.minMaxZoomPreference, const MinMaxZoomPreference(1.0, 3.0)); await tester.pumpWidget( @@ -238,8 +220,8 @@ void main() { ), ); - expect( - platformGoogleMap.minMaxZoomPreference, MinMaxZoomPreference.unbounded); + expect(map.mapConfiguration.minMaxZoomPreference, + MinMaxZoomPreference.unbounded); }); testWidgets('Can update rotateGesturesEnabled', (WidgetTester tester) async { @@ -253,10 +235,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.rotateGesturesEnabled, false); + expect(map.mapConfiguration.rotateGesturesEnabled, false); await tester.pumpWidget( const Directionality( @@ -267,7 +248,7 @@ void main() { ), ); - expect(platformGoogleMap.rotateGesturesEnabled, true); + expect(map.mapConfiguration.rotateGesturesEnabled, true); }); testWidgets('Can update scrollGesturesEnabled', (WidgetTester tester) async { @@ -281,10 +262,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.scrollGesturesEnabled, false); + expect(map.mapConfiguration.scrollGesturesEnabled, false); await tester.pumpWidget( const Directionality( @@ -295,7 +275,7 @@ void main() { ), ); - expect(platformGoogleMap.scrollGesturesEnabled, true); + expect(map.mapConfiguration.scrollGesturesEnabled, true); }); testWidgets('Can update tiltGesturesEnabled', (WidgetTester tester) async { @@ -309,10 +289,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.tiltGesturesEnabled, false); + expect(map.mapConfiguration.tiltGesturesEnabled, false); await tester.pumpWidget( const Directionality( @@ -323,7 +302,7 @@ void main() { ), ); - expect(platformGoogleMap.tiltGesturesEnabled, true); + expect(map.mapConfiguration.tiltGesturesEnabled, true); }); testWidgets('Can update trackCameraPosition', (WidgetTester tester) async { @@ -336,10 +315,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.trackCameraPosition, false); + expect(map.mapConfiguration.trackCameraPosition, false); await tester.pumpWidget( Directionality( @@ -352,7 +330,7 @@ void main() { ), ); - expect(platformGoogleMap.trackCameraPosition, true); + expect(map.mapConfiguration.trackCameraPosition, true); }); testWidgets('Can update zoomGesturesEnabled', (WidgetTester tester) async { @@ -366,10 +344,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.zoomGesturesEnabled, false); + expect(map.mapConfiguration.zoomGesturesEnabled, false); await tester.pumpWidget( const Directionality( @@ -380,7 +357,7 @@ void main() { ), ); - expect(platformGoogleMap.zoomGesturesEnabled, true); + expect(map.mapConfiguration.zoomGesturesEnabled, true); }); testWidgets('Can update zoomControlsEnabled', (WidgetTester tester) async { @@ -394,10 +371,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.zoomControlsEnabled, false); + expect(map.mapConfiguration.zoomControlsEnabled, false); await tester.pumpWidget( const Directionality( @@ -408,7 +384,7 @@ void main() { ), ); - expect(platformGoogleMap.zoomControlsEnabled, true); + expect(map.mapConfiguration.zoomControlsEnabled, true); }); testWidgets('Can update myLocationEnabled', (WidgetTester tester) async { @@ -421,10 +397,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.myLocationEnabled, false); + expect(map.mapConfiguration.myLocationEnabled, false); await tester.pumpWidget( const Directionality( @@ -436,7 +411,7 @@ void main() { ), ); - expect(platformGoogleMap.myLocationEnabled, true); + expect(map.mapConfiguration.myLocationEnabled, true); }); testWidgets('Can update myLocationButtonEnabled', @@ -450,10 +425,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.myLocationButtonEnabled, true); + expect(map.mapConfiguration.myLocationButtonEnabled, true); await tester.pumpWidget( const Directionality( @@ -465,7 +439,7 @@ void main() { ), ); - expect(platformGoogleMap.myLocationButtonEnabled, false); + expect(map.mapConfiguration.myLocationButtonEnabled, false); }); testWidgets('Is default padding 0', (WidgetTester tester) async { @@ -478,10 +452,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.padding, [0, 0, 0, 0]); + expect(map.mapConfiguration.padding, EdgeInsets.zero); }); testWidgets('Can update padding', (WidgetTester tester) async { @@ -494,10 +467,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.padding, [0, 0, 0, 0]); + expect(map.mapConfiguration.padding, EdgeInsets.zero); await tester.pumpWidget( const Directionality( @@ -509,7 +481,8 @@ void main() { ), ); - expect(platformGoogleMap.padding, [20, 10, 40, 30]); + expect(map.mapConfiguration.padding, + const EdgeInsets.fromLTRB(10, 20, 30, 40)); await tester.pumpWidget( const Directionality( @@ -521,7 +494,8 @@ void main() { ), ); - expect(platformGoogleMap.padding, [60, 50, 80, 70]); + expect(map.mapConfiguration.padding, + const EdgeInsets.fromLTRB(50, 60, 70, 80)); }); testWidgets('Can update traffic', (WidgetTester tester) async { @@ -534,10 +508,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.trafficEnabled, false); + expect(map.mapConfiguration.trafficEnabled, false); await tester.pumpWidget( const Directionality( @@ -549,7 +522,7 @@ void main() { ), ); - expect(platformGoogleMap.trafficEnabled, true); + expect(map.mapConfiguration.trafficEnabled, true); }); testWidgets('Can update buildings', (WidgetTester tester) async { @@ -563,10 +536,9 @@ void main() { ), ); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.buildingsEnabled, false); + expect(map.mapConfiguration.buildingsEnabled, false); await tester.pumpWidget( const Directionality( @@ -577,12 +549,6 @@ void main() { ), ); - expect(platformGoogleMap.buildingsEnabled, true); + expect(map.mapConfiguration.buildingsEnabled, true); }); } - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart index 7f88b60ad6c7..eb7e038c0439 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart @@ -2,22 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'package:stream_transform/stream_transform.dart'; + +import 'fake_google_maps_flutter_platform.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - late TestGoogleMapsFlutterPlatform platform; + late FakeGoogleMapsFlutterPlatform platform; setUp(() { // Use a mock platform so we never need to hit the MethodChannel code. - platform = TestGoogleMapsFlutterPlatform(); + platform = FakeGoogleMapsFlutterPlatform(); GoogleMapsFlutterPlatform.instance = platform; }); @@ -66,222 +64,3 @@ void main() { expect(platform.disposed, true); }); } - -// A dummy implementation of the platform interface for tests. -class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { - TestGoogleMapsFlutterPlatform(); - - // The IDs passed to each call to buildView, in call order. - List createdIds = []; - - // Whether `dispose` has been called. - bool disposed = false; - - // Stream controller to inject events for testing. - final StreamController> mapEventStreamController = - StreamController>.broadcast(); - - @override - Future init(int mapId) async {} - - @override - Future updateMapConfiguration( - MapConfiguration update, { - required int mapId, - }) async {} - - @override - Future updateMarkers( - MarkerUpdates markerUpdates, { - required int mapId, - }) async {} - - @override - Future updatePolygons( - PolygonUpdates polygonUpdates, { - required int mapId, - }) async {} - - @override - Future updatePolylines( - PolylineUpdates polylineUpdates, { - required int mapId, - }) async {} - - @override - Future updateCircles( - CircleUpdates circleUpdates, { - required int mapId, - }) async {} - - @override - Future updateTileOverlays({ - required Set newTileOverlays, - required int mapId, - }) async {} - - @override - Future clearTileCache( - TileOverlayId tileOverlayId, { - required int mapId, - }) async {} - - @override - Future animateCamera( - CameraUpdate cameraUpdate, { - required int mapId, - }) async {} - - @override - Future moveCamera( - CameraUpdate cameraUpdate, { - required int mapId, - }) async {} - - @override - Future setMapStyle( - String? mapStyle, { - required int mapId, - }) async {} - - @override - Future getVisibleRegion({ - required int mapId, - }) async { - return LatLngBounds( - southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)); - } - - @override - Future getScreenCoordinate( - LatLng latLng, { - required int mapId, - }) async { - return const ScreenCoordinate(x: 0, y: 0); - } - - @override - Future getLatLng( - ScreenCoordinate screenCoordinate, { - required int mapId, - }) async { - return const LatLng(0, 0); - } - - @override - Future showMarkerInfoWindow( - MarkerId markerId, { - required int mapId, - }) async {} - - @override - Future hideMarkerInfoWindow( - MarkerId markerId, { - required int mapId, - }) async {} - - @override - Future isMarkerInfoWindowShown( - MarkerId markerId, { - required int mapId, - }) async { - return false; - } - - @override - Future getZoomLevel({ - required int mapId, - }) async { - return 0.0; - } - - @override - Future takeSnapshot({ - required int mapId, - }) async { - return null; - } - - @override - Stream onCameraMoveStarted({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onCameraMove({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onCameraIdle({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onMarkerTap({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onInfoWindowTap({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onMarkerDragStart({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onMarkerDrag({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onMarkerDragEnd({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onPolylineTap({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onPolygonTap({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onCircleTap({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onTap({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - Stream onLongPress({required int mapId}) { - return mapEventStreamController.stream.whereType(); - } - - @override - void dispose({required int mapId}) { - disposed = true; - } - - @override - Widget buildViewWithConfiguration( - int creationId, - PlatformViewCreatedCallback onPlatformViewCreated, { - required MapWidgetConfiguration widgetConfiguration, - MapObjects mapObjects = const MapObjects(), - MapConfiguration mapConfiguration = const MapConfiguration(), - }) { - onPlatformViewCreated(0); - createdIds.add(creationId); - return Container(); - } -} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart index 75a153e0eaa2..9f65f5d3bf5b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'fake_maps_controllers.dart'; +import 'fake_google_maps_flutter_platform.dart'; Widget _mapWithMarkers(Set markers) { return Directionality( @@ -20,36 +20,24 @@ Widget _mapWithMarkers(Set markers) { } void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - }); + late FakeGoogleMapsFlutterPlatform platform; setUp(() { - fakePlatformViewsController.reset(); + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; }); testWidgets('Initializing a marker', (WidgetTester tester) async { const Marker m1 = Marker(markerId: MarkerId('marker_1')); await tester.pumpWidget(_mapWithMarkers({m1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.markersToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.markerUpdates.last.markersToAdd.length, 1); - final Marker initializedMarker = platformGoogleMap.markersToAdd.first; + final Marker initializedMarker = map.markerUpdates.last.markersToAdd.first; expect(initializedMarker, equals(m1)); - expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); - expect(platformGoogleMap.markersToChange.isEmpty, true); + expect(map.markerUpdates.last.markerIdsToRemove.isEmpty, true); + expect(map.markerUpdates.last.markersToChange.isEmpty, true); }); testWidgets('Adding a marker', (WidgetTester tester) async { @@ -59,16 +47,15 @@ void main() { await tester.pumpWidget(_mapWithMarkers({m1})); await tester.pumpWidget(_mapWithMarkers({m1, m2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.markersToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.markerUpdates.last.markersToAdd.length, 1); - final Marker addedMarker = platformGoogleMap.markersToAdd.first; + final Marker addedMarker = map.markerUpdates.last.markersToAdd.first; expect(addedMarker, equals(m2)); - expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); + expect(map.markerUpdates.last.markerIdsToRemove.isEmpty, true); - expect(platformGoogleMap.markersToChange.isEmpty, true); + expect(map.markerUpdates.last.markersToChange.isEmpty, true); }); testWidgets('Removing a marker', (WidgetTester tester) async { @@ -77,13 +64,12 @@ void main() { await tester.pumpWidget(_mapWithMarkers({m1})); await tester.pumpWidget(_mapWithMarkers({})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.markerIdsToRemove.length, 1); - expect(platformGoogleMap.markerIdsToRemove.first, equals(m1.markerId)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.markerUpdates.last.markerIdsToRemove.length, 1); + expect(map.markerUpdates.last.markerIdsToRemove.first, equals(m1.markerId)); - expect(platformGoogleMap.markersToChange.isEmpty, true); - expect(platformGoogleMap.markersToAdd.isEmpty, true); + expect(map.markerUpdates.last.markersToChange.isEmpty, true); + expect(map.markerUpdates.last.markersToAdd.isEmpty, true); }); testWidgets('Updating a marker', (WidgetTester tester) async { @@ -93,13 +79,12 @@ void main() { await tester.pumpWidget(_mapWithMarkers({m1})); await tester.pumpWidget(_mapWithMarkers({m2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.markersToChange.length, 1); - expect(platformGoogleMap.markersToChange.first, equals(m2)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.markerUpdates.last.markersToChange.length, 1); + expect(map.markerUpdates.last.markersToChange.first, equals(m2)); - expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); - expect(platformGoogleMap.markersToAdd.isEmpty, true); + expect(map.markerUpdates.last.markerIdsToRemove.isEmpty, true); + expect(map.markerUpdates.last.markersToAdd.isEmpty, true); }); testWidgets('Updating a marker', (WidgetTester tester) async { @@ -112,11 +97,10 @@ void main() { await tester.pumpWidget(_mapWithMarkers({m1})); await tester.pumpWidget(_mapWithMarkers({m2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.markersToChange.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.markerUpdates.last.markersToChange.length, 1); - final Marker update = platformGoogleMap.markersToChange.first; + final Marker update = map.markerUpdates.last.markersToChange.first; expect(update, equals(m2)); expect(update.infoWindow.snippet, 'changed'); }); @@ -132,12 +116,11 @@ void main() { await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.markersToChange, cur); - expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); - expect(platformGoogleMap.markersToAdd.isEmpty, true); + expect(map.markerUpdates.last.markersToChange, cur); + expect(map.markerUpdates.last.markerIdsToRemove.isEmpty, true); + expect(map.markerUpdates.last.markersToAdd.isEmpty, true); }); testWidgets('Multi Update', (WidgetTester tester) async { @@ -153,16 +136,15 @@ void main() { await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.markersToChange.length, 1); - expect(platformGoogleMap.markersToAdd.length, 1); - expect(platformGoogleMap.markerIdsToRemove.length, 1); + expect(map.markerUpdates.last.markersToChange.length, 1); + expect(map.markerUpdates.last.markersToAdd.length, 1); + expect(map.markerUpdates.last.markerIdsToRemove.length, 1); - expect(platformGoogleMap.markersToChange.first, equals(m2)); - expect(platformGoogleMap.markersToAdd.first, equals(m1)); - expect(platformGoogleMap.markerIdsToRemove.first, equals(m3.markerId)); + expect(map.markerUpdates.last.markersToChange.first, equals(m2)); + expect(map.markerUpdates.last.markersToAdd.first, equals(m1)); + expect(map.markerUpdates.last.markerIdsToRemove.first, equals(m3.markerId)); }); testWidgets('Partial Update', (WidgetTester tester) async { @@ -176,12 +158,11 @@ void main() { await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.markersToChange, {m3}); - expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); - expect(platformGoogleMap.markersToAdd.isEmpty, true); + expect(map.markerUpdates.last.markersToChange, {m3}); + expect(map.markerUpdates.last.markerIdsToRemove.isEmpty, true); + expect(map.markerUpdates.last.markersToAdd.isEmpty, true); }); testWidgets('Update non platform related attr', (WidgetTester tester) async { @@ -196,17 +177,43 @@ void main() { await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.markersToChange.isEmpty, true); - expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); - expect(platformGoogleMap.markersToAdd.isEmpty, true); + expect(map.markerUpdates.last.markersToChange.isEmpty, true); + expect(map.markerUpdates.last.markerIdsToRemove.isEmpty, true); + expect(map.markerUpdates.last.markersToAdd.isEmpty, true); }); -} -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; + testWidgets('multi-update with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Marker m1 = Marker(markerId: MarkerId('marker_1')); + const Marker m2 = Marker(markerId: MarkerId('marker_2')); + const Marker m3 = Marker(markerId: MarkerId('marker_3')); + const Marker m3updated = + Marker(markerId: MarkerId('marker_3'), draggable: true); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithMarkers({m1, m2})); + await tester.pumpWidget(_mapWithMarkers({m1, m3})); + await tester.pumpWidget(_mapWithMarkers({m1, m3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.markerUpdates.length, 3); + + expect(map.markerUpdates[0].markersToChange.isEmpty, true); + expect(map.markerUpdates[0].markersToAdd, {m1, m2}); + expect(map.markerUpdates[0].markerIdsToRemove.isEmpty, true); + + expect(map.markerUpdates[1].markersToChange.isEmpty, true); + expect(map.markerUpdates[1].markersToAdd, {m3}); + expect(map.markerUpdates[1].markerIdsToRemove, {m2.markerId}); + + expect(map.markerUpdates[2].markersToChange, {m3updated}); + expect(map.markerUpdates[2].markersToAdd.isEmpty, true); + expect(map.markerUpdates[2].markerIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart index 152cbddfc34a..08910fa5ccbb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'fake_maps_controllers.dart'; +import 'fake_google_maps_flutter_platform.dart'; Widget _mapWithPolygons(Set polygons) { return Directionality( @@ -43,36 +43,25 @@ Polygon _polygonWithPointsAndHole(PolygonId polygonId) { } void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - }); + late FakeGoogleMapsFlutterPlatform platform; setUp(() { - fakePlatformViewsController.reset(); + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; }); testWidgets('Initializing a polygon', (WidgetTester tester) async { const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); await tester.pumpWidget(_mapWithPolygons({p1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToAdd.length, 1); - final Polygon initializedPolygon = platformGoogleMap.polygonsToAdd.first; + final Polygon initializedPolygon = + map.polygonUpdates.last.polygonsToAdd.first; expect(initializedPolygon, equals(p1)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); }); testWidgets('Adding a polygon', (WidgetTester tester) async { @@ -82,16 +71,15 @@ void main() { await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({p1, p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToAdd.length, 1); - final Polygon addedPolygon = platformGoogleMap.polygonsToAdd.first; + final Polygon addedPolygon = map.polygonUpdates.last.polygonsToAdd.first; expect(addedPolygon, equals(p2)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); }); testWidgets('Removing a polygon', (WidgetTester tester) async { @@ -100,13 +88,13 @@ void main() { await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonIdsToRemove.length, 1); - expect(platformGoogleMap.polygonIdsToRemove.first, equals(p1.polygonId)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonIdsToRemove.length, 1); + expect( + map.polygonUpdates.last.polygonIdsToRemove.first, equals(p1.polygonId)); - expect(platformGoogleMap.polygonsToChange.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Updating a polygon', (WidgetTester tester) async { @@ -117,13 +105,12 @@ void main() { await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToChange.length, 1); - expect(platformGoogleMap.polygonsToChange.first, equals(p2)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToChange.length, 1); + expect(map.polygonUpdates.last.polygonsToChange.first, equals(p2)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Mutate a polygon', (WidgetTester tester) async { @@ -137,13 +124,12 @@ void main() { p1.points.add(const LatLng(1.0, 1.0)); await tester.pumpWidget(_mapWithPolygons({p1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToChange.length, 1); - expect(platformGoogleMap.polygonsToChange.first, equals(p1)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToChange.length, 1); + expect(map.polygonUpdates.last.polygonsToChange.first, equals(p1)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Multi Update', (WidgetTester tester) async { @@ -157,12 +143,11 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange, cur); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange, cur); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Multi Update', (WidgetTester tester) async { @@ -178,16 +163,16 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange.length, 1); - expect(platformGoogleMap.polygonsToAdd.length, 1); - expect(platformGoogleMap.polygonIdsToRemove.length, 1); + expect(map.polygonUpdates.last.polygonsToChange.length, 1); + expect(map.polygonUpdates.last.polygonsToAdd.length, 1); + expect(map.polygonUpdates.last.polygonIdsToRemove.length, 1); - expect(platformGoogleMap.polygonsToChange.first, equals(p2)); - expect(platformGoogleMap.polygonsToAdd.first, equals(p1)); - expect(platformGoogleMap.polygonIdsToRemove.first, equals(p3.polygonId)); + expect(map.polygonUpdates.last.polygonsToChange.first, equals(p2)); + expect(map.polygonUpdates.last.polygonsToAdd.first, equals(p1)); + expect( + map.polygonUpdates.last.polygonIdsToRemove.first, equals(p3.polygonId)); }); testWidgets('Partial Update', (WidgetTester tester) async { @@ -201,12 +186,11 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange, {p3}); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange, {p3}); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Update non platform related attr', (WidgetTester tester) async { @@ -218,12 +202,11 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange.isEmpty, true); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Initializing a polygon with points and hole', @@ -231,14 +214,14 @@ void main() { final Polygon p1 = _polygonWithPointsAndHole(const PolygonId('polygon_1')); await tester.pumpWidget(_mapWithPolygons({p1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToAdd.length, 1); - final Polygon initializedPolygon = platformGoogleMap.polygonsToAdd.first; + final Polygon initializedPolygon = + map.polygonUpdates.last.polygonsToAdd.first; expect(initializedPolygon, equals(p1)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); }); testWidgets('Adding a polygon with points and hole', @@ -249,16 +232,15 @@ void main() { await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({p1, p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToAdd.length, 1); - final Polygon addedPolygon = platformGoogleMap.polygonsToAdd.first; + final Polygon addedPolygon = map.polygonUpdates.last.polygonsToAdd.first; expect(addedPolygon, equals(p2)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); }); testWidgets('Removing a polygon with points and hole', @@ -268,13 +250,13 @@ void main() { await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonIdsToRemove.length, 1); - expect(platformGoogleMap.polygonIdsToRemove.first, equals(p1.polygonId)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonIdsToRemove.length, 1); + expect( + map.polygonUpdates.last.polygonIdsToRemove.first, equals(p1.polygonId)); - expect(platformGoogleMap.polygonsToChange.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Updating a polygon by adding points and hole', @@ -285,13 +267,12 @@ void main() { await tester.pumpWidget(_mapWithPolygons({p1})); await tester.pumpWidget(_mapWithPolygons({p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToChange.length, 1); - expect(platformGoogleMap.polygonsToChange.first, equals(p2)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToChange.length, 1); + expect(map.polygonUpdates.last.polygonsToChange.first, equals(p2)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Mutate a polygon with points and holes', @@ -311,13 +292,12 @@ void main() { ..addAll(>[_rectPoints(size: 1)]); await tester.pumpWidget(_mapWithPolygons({p1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToChange.length, 1); - expect(platformGoogleMap.polygonsToChange.first, equals(p1)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polygonUpdates.last.polygonsToChange.length, 1); + expect(map.polygonUpdates.last.polygonsToChange.first, equals(p1)); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Multi Update polygons with points and hole', @@ -339,12 +319,11 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange, cur); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange, cur); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); testWidgets('Multi Update polygons with points and hole', @@ -368,16 +347,16 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange.length, 1); - expect(platformGoogleMap.polygonsToAdd.length, 1); - expect(platformGoogleMap.polygonIdsToRemove.length, 1); + expect(map.polygonUpdates.last.polygonsToChange.length, 1); + expect(map.polygonUpdates.last.polygonsToAdd.length, 1); + expect(map.polygonUpdates.last.polygonIdsToRemove.length, 1); - expect(platformGoogleMap.polygonsToChange.first, equals(p2)); - expect(platformGoogleMap.polygonsToAdd.first, equals(p1)); - expect(platformGoogleMap.polygonIdsToRemove.first, equals(p3.polygonId)); + expect(map.polygonUpdates.last.polygonsToChange.first, equals(p2)); + expect(map.polygonUpdates.last.polygonsToAdd.first, equals(p1)); + expect( + map.polygonUpdates.last.polygonIdsToRemove.first, equals(p3.polygonId)); }); testWidgets('Partial Update polygons with points and hole', @@ -399,17 +378,44 @@ void main() { await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polygonsToChange, {p3}); - expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToChange, {p3}); + expect(map.polygonUpdates.last.polygonIdsToRemove.isEmpty, true); + expect(map.polygonUpdates.last.polygonsToAdd.isEmpty, true); }); -} -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; + testWidgets('multi-update with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Polygon p1 = Polygon(polygonId: PolygonId('polygon_1')); + const Polygon p2 = Polygon(polygonId: PolygonId('polygon_2')); + const Polygon p3 = + Polygon(polygonId: PolygonId('polygon_3'), strokeWidth: 1); + const Polygon p3updated = + Polygon(polygonId: PolygonId('polygon_3'), strokeWidth: 2); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithPolygons({p1, p2})); + await tester.pumpWidget(_mapWithPolygons({p1, p3})); + await tester.pumpWidget(_mapWithPolygons({p1, p3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.polygonUpdates.length, 3); + + expect(map.polygonUpdates[0].polygonsToChange.isEmpty, true); + expect(map.polygonUpdates[0].polygonsToAdd, {p1, p2}); + expect(map.polygonUpdates[0].polygonIdsToRemove.isEmpty, true); + + expect(map.polygonUpdates[1].polygonsToChange.isEmpty, true); + expect(map.polygonUpdates[1].polygonsToAdd, {p3}); + expect(map.polygonUpdates[1].polygonIdsToRemove, {p2.polygonId}); + + expect(map.polygonUpdates[2].polygonsToChange, {p3updated}); + expect(map.polygonUpdates[2].polygonsToAdd.isEmpty, true); + expect(map.polygonUpdates[2].polygonIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart index 03b6c620190a..cac311f7f2ed 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'fake_maps_controllers.dart'; +import 'fake_google_maps_flutter_platform.dart'; Widget _mapWithPolylines(Set polylines) { return Directionality( @@ -20,36 +20,25 @@ Widget _mapWithPolylines(Set polylines) { } void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - }); + late FakeGoogleMapsFlutterPlatform platform; setUp(() { - fakePlatformViewsController.reset(); + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; }); testWidgets('Initializing a polyline', (WidgetTester tester) async { const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); await tester.pumpWidget(_mapWithPolylines({p1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylinesToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polylineUpdates.last.polylinesToAdd.length, 1); - final Polyline initializedPolyline = platformGoogleMap.polylinesToAdd.first; + final Polyline initializedPolyline = + map.polylineUpdates.last.polylinesToAdd.first; expect(initializedPolyline, equals(p1)); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToChange.isEmpty, true); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToChange.isEmpty, true); }); testWidgets('Adding a polyline', (WidgetTester tester) async { @@ -59,16 +48,16 @@ void main() { await tester.pumpWidget(_mapWithPolylines({p1})); await tester.pumpWidget(_mapWithPolylines({p1, p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylinesToAdd.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polylineUpdates.last.polylinesToAdd.length, 1); - final Polyline addedPolyline = platformGoogleMap.polylinesToAdd.first; + final Polyline addedPolyline = + map.polylineUpdates.last.polylinesToAdd.first; expect(addedPolyline, equals(p2)); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToChange.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToChange.isEmpty, true); }); testWidgets('Removing a polyline', (WidgetTester tester) async { @@ -77,13 +66,13 @@ void main() { await tester.pumpWidget(_mapWithPolylines({p1})); await tester.pumpWidget(_mapWithPolylines({})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylineIdsToRemove.length, 1); - expect(platformGoogleMap.polylineIdsToRemove.first, equals(p1.polylineId)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polylineUpdates.last.polylineIdsToRemove.length, 1); + expect(map.polylineUpdates.last.polylineIdsToRemove.first, + equals(p1.polylineId)); - expect(platformGoogleMap.polylinesToChange.isEmpty, true); - expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToChange.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToAdd.isEmpty, true); }); testWidgets('Updating a polyline', (WidgetTester tester) async { @@ -94,13 +83,12 @@ void main() { await tester.pumpWidget(_mapWithPolylines({p1})); await tester.pumpWidget(_mapWithPolylines({p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylinesToChange.length, 1); - expect(platformGoogleMap.polylinesToChange.first, equals(p2)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polylineUpdates.last.polylinesToChange.length, 1); + expect(map.polylineUpdates.last.polylinesToChange.first, equals(p2)); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToAdd.isEmpty, true); }); testWidgets('Updating a polyline', (WidgetTester tester) async { @@ -111,11 +99,10 @@ void main() { await tester.pumpWidget(_mapWithPolylines({p1})); await tester.pumpWidget(_mapWithPolylines({p2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylinesToChange.length, 1); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polylineUpdates.last.polylinesToChange.length, 1); - final Polyline update = platformGoogleMap.polylinesToChange.first; + final Polyline update = map.polylineUpdates.last.polylinesToChange.first; expect(update, equals(p2)); expect(update.geodesic, true); }); @@ -131,13 +118,12 @@ void main() { p1.points.add(const LatLng(1.0, 1.0)); await tester.pumpWidget(_mapWithPolylines({p1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylinesToChange.length, 1); - expect(platformGoogleMap.polylinesToChange.first, equals(p1)); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.polylineUpdates.last.polylinesToChange.length, 1); + expect(map.polylineUpdates.last.polylinesToChange.first, equals(p1)); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToAdd.isEmpty, true); }); testWidgets('Multi Update', (WidgetTester tester) async { @@ -151,12 +137,11 @@ void main() { await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polylinesToChange, cur); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToChange, cur); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToAdd.isEmpty, true); }); testWidgets('Multi Update', (WidgetTester tester) async { @@ -172,16 +157,16 @@ void main() { await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polylinesToChange.length, 1); - expect(platformGoogleMap.polylinesToAdd.length, 1); - expect(platformGoogleMap.polylineIdsToRemove.length, 1); + expect(map.polylineUpdates.last.polylinesToChange.length, 1); + expect(map.polylineUpdates.last.polylinesToAdd.length, 1); + expect(map.polylineUpdates.last.polylineIdsToRemove.length, 1); - expect(platformGoogleMap.polylinesToChange.first, equals(p2)); - expect(platformGoogleMap.polylinesToAdd.first, equals(p1)); - expect(platformGoogleMap.polylineIdsToRemove.first, equals(p3.polylineId)); + expect(map.polylineUpdates.last.polylinesToChange.first, equals(p2)); + expect(map.polylineUpdates.last.polylinesToAdd.first, equals(p1)); + expect(map.polylineUpdates.last.polylineIdsToRemove.first, + equals(p3.polylineId)); }); testWidgets('Partial Update', (WidgetTester tester) async { @@ -195,12 +180,11 @@ void main() { await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polylinesToChange, {p3}); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToChange, {p3}); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToAdd.isEmpty, true); }); testWidgets('Update non platform related attr', (WidgetTester tester) async { @@ -212,17 +196,45 @@ void main() { await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; + final PlatformMapStateRecorder map = platform.lastCreatedMap; - expect(platformGoogleMap.polylinesToChange.isEmpty, true); - expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); - expect(platformGoogleMap.polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToChange.isEmpty, true); + expect(map.polylineUpdates.last.polylineIdsToRemove.isEmpty, true); + expect(map.polylineUpdates.last.polylinesToAdd.isEmpty, true); }); -} -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; + testWidgets('multi-update with delays', (WidgetTester tester) async { + platform.simulatePlatformDelay = true; + + const Polyline p1 = Polyline(polylineId: PolylineId('polyline_1')); + const Polyline p2 = Polyline(polylineId: PolylineId('polyline_2')); + const Polyline p3 = + Polyline(polylineId: PolylineId('polyline_3'), width: 1); + const Polyline p3updated = + Polyline(polylineId: PolylineId('polyline_3'), width: 2); + + // First remove one and add another, then update the new one. + await tester.pumpWidget(_mapWithPolylines({p1, p2})); + await tester.pumpWidget(_mapWithPolylines({p1, p3})); + await tester.pumpWidget(_mapWithPolylines({p1, p3updated})); + + final PlatformMapStateRecorder map = platform.lastCreatedMap; + + expect(map.polylineUpdates.length, 3); + + expect(map.polylineUpdates[0].polylinesToChange.isEmpty, true); + expect(map.polylineUpdates[0].polylinesToAdd, {p1, p2}); + expect(map.polylineUpdates[0].polylineIdsToRemove.isEmpty, true); + + expect(map.polylineUpdates[1].polylinesToChange.isEmpty, true); + expect(map.polylineUpdates[1].polylinesToAdd, {p3}); + expect(map.polylineUpdates[1].polylineIdsToRemove, + {p2.polylineId}); + + expect(map.polylineUpdates[2].polylinesToChange, {p3updated}); + expect(map.polylineUpdates[2].polylinesToAdd.isEmpty, true); + expect(map.polylineUpdates[2].polylineIdsToRemove.isEmpty, true); + + await tester.pumpAndSettle(); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart index e4e4514dd501..c2faca593bc6 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'fake_maps_controllers.dart'; +import 'fake_google_maps_flutter_platform.dart'; Widget _mapWithTileOverlays(Set tileOverlays) { return Directionality( @@ -20,20 +20,11 @@ Widget _mapWithTileOverlays(Set tileOverlays) { } void main() { - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - SystemChannels.platform_views, - fakePlatformViewsController.fakePlatformViewsMethodHandler, - ); - }); + late FakeGoogleMapsFlutterPlatform platform; setUp(() { - fakePlatformViewsController.reset(); + platform = FakeGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; }); testWidgets('Initializing a tile overlay', (WidgetTester tester) async { @@ -41,15 +32,8 @@ void main() { TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); await tester.pumpWidget(_mapWithTileOverlays({t1})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.tileOverlaysToAdd.length, 1); - - final TileOverlay initializedTileOverlay = - platformGoogleMap.tileOverlaysToAdd.first; - expect(initializedTileOverlay, equals(t1)); - expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); - expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.tileOverlaySets.last, equals({t1})); }); testWidgets('Adding a tile overlay', (WidgetTester tester) async { @@ -61,16 +45,8 @@ void main() { await tester.pumpWidget(_mapWithTileOverlays({t1})); await tester.pumpWidget(_mapWithTileOverlays({t1, t2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.tileOverlaysToAdd.length, 1); - - final TileOverlay addedTileOverlay = - platformGoogleMap.tileOverlaysToAdd.first; - expect(addedTileOverlay, equals(t2)); - expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); - - expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.tileOverlaySets.last, equals({t1, t2})); }); testWidgets('Removing a tile overlay', (WidgetTester tester) async { @@ -80,32 +56,8 @@ void main() { await tester.pumpWidget(_mapWithTileOverlays({t1})); await tester.pumpWidget(_mapWithTileOverlays({})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.tileOverlayIdsToRemove.length, 1); - expect(platformGoogleMap.tileOverlayIdsToRemove.first, - equals(t1.tileOverlayId)); - - expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); - expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); - }); - - testWidgets('Updating a tile overlay', (WidgetTester tester) async { - const TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); - const TileOverlay t2 = - TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1'), zIndex: 10); - - await tester.pumpWidget(_mapWithTileOverlays({t1})); - await tester.pumpWidget(_mapWithTileOverlays({t2})); - - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.tileOverlaysToChange.length, 1); - expect(platformGoogleMap.tileOverlaysToChange.first, equals(t2)); - - expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); - expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.tileOverlaySets.last, equals({})); }); testWidgets('Updating a tile overlay', (WidgetTester tester) async { @@ -117,94 +69,7 @@ void main() { await tester.pumpWidget(_mapWithTileOverlays({t1})); await tester.pumpWidget(_mapWithTileOverlays({t2})); - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.tileOverlaysToChange.length, 1); - - final TileOverlay update = platformGoogleMap.tileOverlaysToChange.first; - expect(update, equals(t2)); - expect(update.zIndex, 10); - }); - - testWidgets('Multi Update', (WidgetTester tester) async { - TileOverlay t1 = - const TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); - TileOverlay t2 = - const TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_2')); - final Set prev = {t1, t2}; - t1 = const TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_1'), visible: false); - t2 = const TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_2'), zIndex: 10); - final Set cur = {t1, t2}; - - await tester.pumpWidget(_mapWithTileOverlays(prev)); - await tester.pumpWidget(_mapWithTileOverlays(cur)); - - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - - expect(platformGoogleMap.tileOverlaysToChange, cur); - expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); - expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); - }); - - testWidgets('Multi Update', (WidgetTester tester) async { - TileOverlay t2 = - const TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_2')); - const TileOverlay t3 = - TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_3')); - final Set prev = {t2, t3}; - - // t1 is added, t2 is updated, t3 is removed. - const TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); - t2 = const TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_2'), zIndex: 10); - final Set cur = {t1, t2}; - - await tester.pumpWidget(_mapWithTileOverlays(prev)); - await tester.pumpWidget(_mapWithTileOverlays(cur)); - - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - - expect(platformGoogleMap.tileOverlaysToChange.length, 1); - expect(platformGoogleMap.tileOverlaysToAdd.length, 1); - expect(platformGoogleMap.tileOverlayIdsToRemove.length, 1); - - expect(platformGoogleMap.tileOverlaysToChange.first, equals(t2)); - expect(platformGoogleMap.tileOverlaysToAdd.first, equals(t1)); - expect(platformGoogleMap.tileOverlayIdsToRemove.first, - equals(t3.tileOverlayId)); - }); - - testWidgets('Partial Update', (WidgetTester tester) async { - const TileOverlay t1 = - TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_1')); - const TileOverlay t2 = - TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_2')); - TileOverlay t3 = - const TileOverlay(tileOverlayId: TileOverlayId('tile_overlay_3')); - final Set prev = {t1, t2, t3}; - t3 = const TileOverlay( - tileOverlayId: TileOverlayId('tile_overlay_3'), zIndex: 10); - final Set cur = {t1, t2, t3}; - - await tester.pumpWidget(_mapWithTileOverlays(prev)); - await tester.pumpWidget(_mapWithTileOverlays(cur)); - - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView!; - - expect(platformGoogleMap.tileOverlaysToChange, {t3}); - expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); - expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + final PlatformMapStateRecorder map = platform.lastCreatedMap; + expect(map.tileOverlaySets.last, equals({t2})); }); } - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml index 525576f013cd..dab6b69b98ba 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -25,3 +25,10 @@ dev_dependencies: integration_test: sdk: flutter mockito: 5.4.1 + +dependency_overrides: + # Override the google_maps_flutter dependency on google_maps_flutter_web. + # TODO(ditman): Unwind the circular dependency. This will create problems + # if we need to make a breaking change to google_maps_flutter_web. + google_maps_flutter_web: + path: ../ From 59d93d64cb835877c7c8187a9383dab1505959b5 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 16 Jun 2023 09:01:25 -0400 Subject: [PATCH 43/53] [tool] Add command aliases (#4207) Adds aliases for all commands that put the verb first, since currently they are inconsistent which can make it hard to remember (in particular, I often write `check-foo` instead of `foo-check` when running locally, and it fails). Also does the long-overdue renaming of `test` to `dart-test`, since we now have `native-test`, `custom-test`, etc. `test` continues to work as an alias for individual muscle memory. --- .ci/scripts/dart_unit_tests_win32.sh | 2 +- .cirrus.yml | 4 +- script/tool/lib/src/custom_test_command.dart | 3 ++ ...st_command.dart => dart_test_command.dart} | 12 +++-- .../lib/src/dependabot_check_command.dart | 3 ++ .../src/federation_safety_check_command.dart | 3 ++ script/tool/lib/src/gradle_check_command.dart | 3 ++ .../tool/lib/src/license_check_command.dart | 3 ++ script/tool/lib/src/main.dart | 4 +- script/tool/lib/src/native_test_command.dart | 3 ++ .../tool/lib/src/podspec_check_command.dart | 2 +- .../tool/lib/src/publish_check_command.dart | 3 ++ .../tool/lib/src/pubspec_check_command.dart | 7 ++- script/tool/lib/src/readme_check_command.dart | 3 ++ .../tool/lib/src/version_check_command.dart | 3 ++ .../tool/lib/src/xcode_analyze_command.dart | 3 ++ ..._test.dart => dart_test_command_test.dart} | 47 ++++++++++++------- 17 files changed, 81 insertions(+), 27 deletions(-) rename script/tool/lib/src/{test_command.dart => dart_test_command.dart} (89%) rename script/tool/test/{test_command_test.dart => dart_test_command_test.dart} (85%) diff --git a/.ci/scripts/dart_unit_tests_win32.sh b/.ci/scripts/dart_unit_tests_win32.sh index 5fbe4764f6b3..2cd451c45caa 100755 --- a/.ci/scripts/dart_unit_tests_win32.sh +++ b/.ci/scripts/dart_unit_tests_win32.sh @@ -4,6 +4,6 @@ # found in the LICENSE file. set -e -dart ./script/tool/bin/flutter_plugin_tools.dart test \ +dart ./script/tool/bin/flutter_plugin_tools.dart dart-test \ --exclude=script/configs/windows_unit_tests_exceptions.yaml \ --packages-for-branch --log-timing diff --git a/.cirrus.yml b/.cirrus.yml index 47ee7ff760b8..d7ee3cc6ff00 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -231,13 +231,13 @@ task: CHANNEL: "master" CHANNEL: "stable" unit_test_script: - - ./script/tool_runner.sh test --exclude=script/configs/dart_unit_tests_exceptions.yaml + - ./script/tool_runner.sh dart-test --exclude=script/configs/dart_unit_tests_exceptions.yaml pathified_unit_test_script: # Run tests with path-based dependencies to ensure that publishing # the changes won't break tests of other packages in the repository # that depend on it. - ./script/tool_runner.sh make-deps-path-based --target-dependencies-with-non-breaking-updates - - $PLUGIN_TOOL_COMMAND test --run-on-dirty-packages --exclude=script/configs/dart_unit_tests_exceptions.yaml + - $PLUGIN_TOOL_COMMAND dart-test --run-on-dirty-packages --exclude=script/configs/dart_unit_tests_exceptions.yaml - name: linux-custom_package_tests env: PATH: $PATH:/usr/local/bin diff --git a/script/tool/lib/src/custom_test_command.dart b/script/tool/lib/src/custom_test_command.dart index 0ef6e602c070..aac8fa8122c3 100644 --- a/script/tool/lib/src/custom_test_command.dart +++ b/script/tool/lib/src/custom_test_command.dart @@ -28,6 +28,9 @@ class CustomTestCommand extends PackageLoopingCommand { @override final String name = 'custom-test'; + @override + List get aliases => ['test-custom']; + @override final String description = 'Runs package-specific custom tests defined in ' "a package's tool/$_scriptName file.\n\n" diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/dart_test_command.dart similarity index 89% rename from script/tool/lib/src/test_command.dart rename to script/tool/lib/src/dart_test_command.dart index 5c793f63ed4b..9a93d2d9a2aa 100644 --- a/script/tool/lib/src/test_command.dart +++ b/script/tool/lib/src/dart_test_command.dart @@ -12,9 +12,9 @@ import 'common/process_runner.dart'; import 'common/repository_package.dart'; /// A command to run Dart unit tests for packages. -class TestCommand extends PackageLoopingCommand { +class DartTestCommand extends PackageLoopingCommand { /// Creates an instance of the test command. - TestCommand( + DartTestCommand( Directory packagesDir, { ProcessRunner processRunner = const ProcessRunner(), Platform platform = const LocalPlatform(), @@ -30,7 +30,13 @@ class TestCommand extends PackageLoopingCommand { } @override - final String name = 'test'; + final String name = 'dart-test'; + + // TODO(stuartmorgan): Eventually remove 'test', which is a legacy name from + // before there were other test commands that made it ambiguous. For now it's + // an alias to avoid breaking people's workflows. + @override + List get aliases => ['test', 'test-dart']; @override final String description = 'Runs the Dart tests for all packages.\n\n' diff --git a/script/tool/lib/src/dependabot_check_command.dart b/script/tool/lib/src/dependabot_check_command.dart index 77b44e11b59e..d16fb33e92f3 100644 --- a/script/tool/lib/src/dependabot_check_command.dart +++ b/script/tool/lib/src/dependabot_check_command.dart @@ -32,6 +32,9 @@ class DependabotCheckCommand extends PackageLoopingCommand { @override final String name = 'dependabot-check'; + @override + List get aliases => ['check-dependabot']; + @override final String description = 'Checks that all packages have Dependabot coverage.'; diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart index 30d4d178d065..837193b1cccc 100644 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ b/script/tool/lib/src/federation_safety_check_command.dart @@ -52,6 +52,9 @@ class FederationSafetyCheckCommand extends PackageLoopingCommand { @override final String name = 'federation-safety-check'; + @override + List get aliases => ['check-federation-safety']; + @override final String description = 'Checks that the change does not violate repository rules around changes ' diff --git a/script/tool/lib/src/gradle_check_command.dart b/script/tool/lib/src/gradle_check_command.dart index 53da6405beb7..cbce766b3763 100644 --- a/script/tool/lib/src/gradle_check_command.dart +++ b/script/tool/lib/src/gradle_check_command.dart @@ -23,6 +23,9 @@ class GradleCheckCommand extends PackageLoopingCommand { @override final String name = 'gradle-check'; + @override + List get aliases => ['check-gradle']; + @override final String description = 'Checks that gradle files follow repository conventions.'; diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart index 0517bcf43298..a8d4a7be5f62 100644 --- a/script/tool/lib/src/license_check_command.dart +++ b/script/tool/lib/src/license_check_command.dart @@ -114,6 +114,9 @@ class LicenseCheckCommand extends PackageCommand { @override final String name = 'license-check'; + @override + List get aliases => ['check-license']; + @override final String description = 'Ensures that all code files have copyright/license blocks.'; diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index c86629d4f4a0..78fa5a3f0c7b 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -13,6 +13,7 @@ import 'build_examples_command.dart'; import 'common/core.dart'; import 'create_all_packages_app_command.dart'; import 'custom_test_command.dart'; +import 'dart_test_command.dart'; import 'dependabot_check_command.dart'; import 'drive_examples_command.dart'; import 'federation_safety_check_command.dart'; @@ -31,7 +32,6 @@ import 'publish_command.dart'; import 'pubspec_check_command.dart'; import 'readme_check_command.dart'; import 'remove_dev_dependencies_command.dart'; -import 'test_command.dart'; import 'update_dependency_command.dart'; import 'update_excerpts_command.dart'; import 'update_min_sdk_command.dart'; @@ -80,7 +80,7 @@ void main(List args) { ..addCommand(PubspecCheckCommand(packagesDir)) ..addCommand(ReadmeCheckCommand(packagesDir)) ..addCommand(RemoveDevDependenciesCommand(packagesDir)) - ..addCommand(TestCommand(packagesDir)) + ..addCommand(DartTestCommand(packagesDir)) ..addCommand(UpdateDependencyCommand(packagesDir)) ..addCommand(UpdateExcerptsCommand(packagesDir)) ..addCommand(UpdateMinSdkCommand(packagesDir)) diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart index f6eb3c164e24..d5f720a02e3a 100644 --- a/script/tool/lib/src/native_test_command.dart +++ b/script/tool/lib/src/native_test_command.dart @@ -73,6 +73,9 @@ class NativeTestCommand extends PackageLoopingCommand { @override final String name = 'native-test'; + @override + List get aliases => ['test-native']; + @override final String description = ''' Runs native unit tests and native integration tests. diff --git a/script/tool/lib/src/podspec_check_command.dart b/script/tool/lib/src/podspec_check_command.dart index dda08eee32be..9fcaa469955c 100644 --- a/script/tool/lib/src/podspec_check_command.dart +++ b/script/tool/lib/src/podspec_check_command.dart @@ -31,7 +31,7 @@ class PodspecCheckCommand extends PackageLoopingCommand { final String name = 'podspec-check'; @override - List get aliases => ['podspec', 'podspecs']; + List get aliases => ['podspec', 'podspecs', 'check-podspec']; @override final String description = diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index 2d7f3ca47f29..0ac01535b445 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -53,6 +53,9 @@ class PublishCheckCommand extends PackageLoopingCommand { @override final String name = 'publish-check'; + @override + List get aliases => ['check-publish']; + @override final String description = 'Checks to make sure that a package *could* be published.'; diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart index c56fa2723bb2..838aac0541b1 100644 --- a/script/tool/lib/src/pubspec_check_command.dart +++ b/script/tool/lib/src/pubspec_check_command.dart @@ -89,6 +89,9 @@ class PubspecCheckCommand extends PackageLoopingCommand { @override final String name = 'pubspec-check'; + @override + List get aliases => ['check-pubspec']; + @override final String description = 'Checks that pubspecs follow repository conventions.'; @@ -104,8 +107,8 @@ class PubspecCheckCommand extends PackageLoopingCommand { Future initializeRun() async { // Find all local, published packages. for (final File pubspecFile in (await packagesDir.parent - .list(recursive: true, followLinks: false) - .toList()) + .list(recursive: true, followLinks: false) + .toList()) .whereType() .where((File entity) => p.basename(entity.path) == 'pubspec.yaml')) { final Pubspec? pubspec = _tryParsePubspec(pubspecFile.readAsStringSync()); diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart index 3c119f04289f..7e28979c9565 100644 --- a/script/tool/lib/src/readme_check_command.dart +++ b/script/tool/lib/src/readme_check_command.dart @@ -48,6 +48,9 @@ class ReadmeCheckCommand extends PackageLoopingCommand { @override final String name = 'readme-check'; + @override + List get aliases => ['check-readme']; + @override final String description = 'Checks that READMEs follow repository conventions.'; diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart index fa9f43519499..a2ea016c3311 100644 --- a/script/tool/lib/src/version_check_command.dart +++ b/script/tool/lib/src/version_check_command.dart @@ -171,6 +171,9 @@ class VersionCheckCommand extends PackageLoopingCommand { @override final String name = 'version-check'; + @override + List get aliases => ['check-version']; + @override final String description = 'Checks if the versions of packages have been incremented per pub specification.\n' diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart index a81bf15477af..7a9a5953f526 100644 --- a/script/tool/lib/src/xcode_analyze_command.dart +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -42,6 +42,9 @@ class XcodeAnalyzeCommand extends PackageLoopingCommand { @override final String name = 'xcode-analyze'; + @override + List get aliases => ['analyze-xcode']; + @override final String description = 'Runs Xcode analysis on the iOS and/or macOS example apps.'; diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/dart_test_command_test.dart similarity index 85% rename from script/tool/test/test_command_test.dart rename to script/tool/test/dart_test_command_test.dart index c4c655bf719f..7f52fcb176c6 100644 --- a/script/tool/test/test_command_test.dart +++ b/script/tool/test/dart_test_command_test.dart @@ -7,7 +7,7 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_plugin_tools/src/common/core.dart'; import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/test_command.dart'; +import 'package:flutter_plugin_tools/src/dart_test_command.dart'; import 'package:platform/platform.dart'; import 'package:test/test.dart'; @@ -27,23 +27,38 @@ void main() { mockPlatform = MockPlatform(); packagesDir = createPackagesDirectory(fileSystem: fileSystem); processRunner = RecordingProcessRunner(); - final TestCommand command = TestCommand( + final DartTestCommand command = DartTestCommand( packagesDir, processRunner: processRunner, platform: mockPlatform, ); - runner = CommandRunner('test_test', 'Test for $TestCommand'); + runner = CommandRunner('test_test', 'Test for $DartTestCommand'); runner.addCommand(command); }); + test('legacy "test" name still works', () async { + final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, + extraFiles: ['test/a_test.dart']); + + await runCapturingPrint(runner, ['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], plugin.path), + ]), + ); + }); + test('runs flutter test on each plugin', () async { final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, extraFiles: ['test/empty_test.dart']); final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, extraFiles: ['test/empty_test.dart']); - await runCapturingPrint(runner, ['test']); + await runCapturingPrint(runner, ['dart-test']); expect( processRunner.recordedCalls, @@ -63,7 +78,7 @@ void main() { 'example/test/an_example_test.dart' ]); - await runCapturingPrint(runner, ['test']); + await runCapturingPrint(runner, ['dart-test']); expect( processRunner.recordedCalls, @@ -86,13 +101,13 @@ void main() { .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = [ FakeProcessInfo( - MockProcess(exitCode: 1), ['test']), // plugin 1 test - FakeProcessInfo(MockProcess(), ['test']), // plugin 2 test + MockProcess(exitCode: 1), ['dart-test']), // plugin 1 test + FakeProcessInfo(MockProcess(), ['dart-test']), // plugin 2 test ]; Error? commandError; final List output = await runCapturingPrint( - runner, ['test'], errorHandler: (Error e) { + runner, ['dart-test'], errorHandler: (Error e) { commandError = e; }); @@ -110,7 +125,7 @@ void main() { final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, extraFiles: ['test/empty_test.dart']); - await runCapturingPrint(runner, ['test']); + await runCapturingPrint(runner, ['dart-test']); expect( processRunner.recordedCalls, @@ -128,7 +143,7 @@ void main() { extraFiles: ['test/empty_test.dart']); await runCapturingPrint( - runner, ['test', '--enable-experiment=exp1']); + runner, ['dart-test', '--enable-experiment=exp1']); expect( processRunner.recordedCalls, @@ -153,7 +168,7 @@ void main() { 'example/test/an_example_test.dart' ]); - await runCapturingPrint(runner, ['test']); + await runCapturingPrint(runner, ['dart-test']); expect( processRunner.recordedCalls, @@ -178,7 +193,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint( - runner, ['test'], errorHandler: (Error e) { + runner, ['dart-test'], errorHandler: (Error e) { commandError = e; }); @@ -203,7 +218,7 @@ void main() { Error? commandError; final List output = await runCapturingPrint( - runner, ['test'], errorHandler: (Error e) { + runner, ['dart-test'], errorHandler: (Error e) { commandError = e; }); @@ -226,7 +241,7 @@ void main() { }, ); - await runCapturingPrint(runner, ['test']); + await runCapturingPrint(runner, ['dart-test']); expect( processRunner.recordedCalls, @@ -249,7 +264,7 @@ void main() { }, ); - await runCapturingPrint(runner, ['test']); + await runCapturingPrint(runner, ['dart-test']); expect( processRunner.recordedCalls, @@ -267,7 +282,7 @@ void main() { extraFiles: ['test/empty_test.dart']); await runCapturingPrint( - runner, ['test', '--enable-experiment=exp1']); + runner, ['dart-test', '--enable-experiment=exp1']); expect( processRunner.recordedCalls, From 08ab0af759cb1c5137da0d341e6d71f83432ccd8 Mon Sep 17 00:00:00 2001 From: engine-flutter-autoroll Date: Fri, 16 Jun 2023 15:39:54 -0400 Subject: [PATCH 44/53] Roll Flutter from b0188cd18274 to fc8856eb80d3 (14 revisions) (#4231) https://github.com/flutter/flutter/compare/b0188cd18274...fc8856eb80d3 2023-06-16 mdebbar@google.com [web] Don't crash on `const HtmlElementView()` (flutter/flutter#128965) 2023-06-16 engine-flutter-autoroll@skia.org Roll Packages from 050729760b25 to f9314a3b833a (3 revisions) (flutter/flutter#128878) 2023-06-16 polinach@google.com Update getProperties to handle Diagnosticable as input. (flutter/flutter#128897) 2023-06-15 engine-flutter-autoroll@skia.org Roll Flutter Engine from 48e0b4e66422 to fb5fed432e59 (1 revision) (flutter/flutter#128967) 2023-06-15 dery.ra@gmail.com Fix dart pub cache clean command on pub.dart (flutter/flutter#128171) 2023-06-15 christopherfujino@gmail.com [flutter_tools] Migrate more integration tests to process result matcher (flutter/flutter#128737) 2023-06-15 christopherfujino@gmail.com [flutter_tools] refactor license collector (flutter/flutter#128748) 2023-06-15 36861262+QuncCccccc@users.noreply.github.com Set Semantics.button to true for date widget (flutter/flutter#128824) 2023-06-15 36861262+QuncCccccc@users.noreply.github.com Update golden tests (flutter/flutter#128914) 2023-06-15 engine-flutter-autoroll@skia.org Roll Flutter Engine from 9934c0de738c to 48e0b4e66422 (26 revisions) (flutter/flutter#128959) 2023-06-15 ian@hixie.ch flutter update-packages --cherry-pick-package (flutter/flutter#128917) 2023-06-15 christopherfujino@gmail.com add .pub-cache back to .gitignore (flutter/flutter#128894) 2023-06-15 engine-flutter-autoroll@skia.org Roll Flutter Engine from 2d8d5ecfe4a8 to 9934c0de738c (2 revisions) (flutter/flutter#128849) 2023-06-15 mdebbar@google.com flutter update-packages --force-upgrade (flutter/flutter#128908) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC rmistry@google.com,stuartmorgan@google.com,tarrinneal@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md --- .ci/flutter_master.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 9c4319ac01a7..2c19000f05f9 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -b0188cd18274d44af9997440fa11fe29cbc504f6 +fc8856eb80d306ac40563582a1212a07141d9001 From 454e6a4a20a21eec474e5a287b97ecfb47fd0563 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 16 Jun 2023 19:13:13 -0400 Subject: [PATCH 45/53] [ci] Introduce LUCI version of Linux build-all-packages tests (#4229) Adds a desktop Linux platform configuration, and a LUCI version of the Linux desktop build-all test. Split from https://github.com/flutter/packages/pull/4223 Part of https://github.com/flutter/flutter/issues/114373 --- .ci.yaml | 33 ++++++++++++++++++++++- .ci/targets/linux_build_all_packages.yaml | 11 ++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 .ci/targets/linux_build_all_packages.yaml diff --git a/.ci.yaml b/.ci.yaml index e44c1479c26b..359b515013e0 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -12,6 +12,18 @@ platform_properties: linux: properties: os: Linux + linux_desktop: + properties: + os: Ubuntu + cores: "8" + device_type: none + dependencies: >- + [ + {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, + {"dependency": "cmake", "version": "build_id:8787856497187628321"}, + {"dependency": "ninja", "version": "version:1.9.0"}, + {"dependency": "curl", "version": "version:7.64.0"} + ] windows: properties: dependencies: > @@ -48,7 +60,7 @@ platform_properties: } targets: - ### Linux tasks ### + ### Linux-host tasks ### - name: Linux repo_tools_tests recipe: packages/packages timeout: 30 @@ -58,6 +70,25 @@ targets: channel: master version_file: flutter_master.version + ### Linux desktop tasks + - name: Linux_desktop build_all_packages master + bringup: true # New target + recipe: packages/packages + timeout: 30 + properties: + version_file: flutter_master.version + target_file: linux_build_all_packages.yaml + channel: master + + - name: Linux_desktop build_all_packages stable + bringup: true # New target + recipe: packages/packages + timeout: 30 + properties: + version_file: flutter_stable.version + target_file: linux_build_all_packages.yaml + channel: stable + ### iOS+macOS tasks ### # TODO(stuartmorgan): Move this to ARM once google_maps_flutter has ARM # support. `pod lint` makes a synthetic target that doesn't respect the diff --git a/.ci/targets/linux_build_all_packages.yaml b/.ci/targets/linux_build_all_packages.yaml new file mode 100644 index 000000000000..b54f7b1e56c2 --- /dev/null +++ b/.ci/targets/linux_build_all_packages.yaml @@ -0,0 +1,11 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: create all_packages app + script: .ci/scripts/create_all_packages_app.sh + - name: build all_packages for Linux debug + script: .ci/scripts/build_all_packages_app.sh + args: ["linux", "debug"] + - name: build all_packages for Linux release + script: .ci/scripts/build_all_packages_app.sh + args: ["linux", "release"] From ed739acac3da895439eab54d7dc19fb2dd18bd19 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 16 Jun 2023 21:06:59 -0400 Subject: [PATCH 46/53] [ci] Add LUCI version of build-all for web (#4232) Adds a new Linux_web configuration, and an initial test (build-all-packages) using it. This is the simplest web test since it doesn't actually run anything, so starting with this one as a foundation to test future web-based tests. Part of https://github.com/flutter/flutter/issues/114373 --- .ci.yaml | 28 +++++++++++++++++++++++++ .ci/targets/web_build_all_packages.yaml | 10 +++++++++ 2 files changed, 38 insertions(+) create mode 100644 .ci/targets/web_build_all_packages.yaml diff --git a/.ci.yaml b/.ci.yaml index 359b515013e0..8dc53d81b7cf 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -24,6 +24,15 @@ platform_properties: {"dependency": "ninja", "version": "version:1.9.0"}, {"dependency": "curl", "version": "version:7.64.0"} ] + linux_web: + properties: + os: Ubuntu + cores: "8" + device_type: none + dependencies: >- + [ + {"dependency": "chrome_and_driver", "version": "version:114.0"} + ] windows: properties: dependencies: > @@ -70,6 +79,25 @@ targets: channel: master version_file: flutter_master.version + ### Web tasks ### + - name: Linux_web web_build_all_packages master + bringup: true # New target + recipe: packages/packages + timeout: 30 + properties: + version_file: flutter_master.version + target_file: web_build_all_packages.yaml + channel: master + + - name: Linux_web web_build_all_packages stable + bringup: true # New target + recipe: packages/packages + timeout: 30 + properties: + version_file: flutter_stable.version + target_file: web_build_all_packages.yaml + channel: stable + ### Linux desktop tasks - name: Linux_desktop build_all_packages master bringup: true # New target diff --git a/.ci/targets/web_build_all_packages.yaml b/.ci/targets/web_build_all_packages.yaml new file mode 100644 index 000000000000..d3b7ae010725 --- /dev/null +++ b/.ci/targets/web_build_all_packages.yaml @@ -0,0 +1,10 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: create all_packages app + script: .ci/scripts/create_all_packages_app.sh + # No debug version, unlike the other platforms, since web does not support + # debug builds. + - name: build all_packages app for Web release + script: .ci/scripts/build_all_packages_app.sh + args: ["web", "release"] From 7365b616fce10e0a1eb20ccf65d76d74d0c354b5 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Fri, 16 Jun 2023 20:20:32 -0700 Subject: [PATCH 47/53] [url_launcher] Remove deprecated onPlatformMessage calls (#4233) The framework has been fully migrated to ChannelBuffers about 2 years ago, meaning all Flutter versions supported by this package (>=3.3.0) only use ChannelBuffers and not the old `onPlatformMessage` call. Therefore, the old calls can now just be removed. (This surfaced because we only remembered just now to deprecate the old long unused API.) --- .../url_launcher_platform_interface/CHANGELOG.md | 3 ++- .../url_launcher_platform_interface/lib/link.dart | 14 +------------- .../url_launcher_platform_interface/pubspec.yaml | 2 +- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md index fa03c093e91e..fb5e36bec5c5 100644 --- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md @@ -1,7 +1,8 @@ -## NEXT +## 2.1.3 * Updates minimum Flutter version to 3.3. * Aligns Dart and Flutter SDK constraints. +* Removes deprecated API calls. ## 2.1.2 diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart index aacd55af39ea..a82c16ed3f10 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart @@ -74,8 +74,6 @@ abstract class LinkInfo { bool get isDisabled; } -typedef _SendMessage = Function(String, ByteData?, void Function(ByteData?)); - /// Pushes the [routeName] into Flutter's navigation system via a platform /// message. /// @@ -91,11 +89,7 @@ Future pushRouteNameToFramework(Object? _, String routeName) { // https://github.com/flutter/flutter/issues/124045. // ignore: deprecated_member_use SystemNavigator.routeInformationUpdated(location: routeName); - final _SendMessage sendMessage = _ambiguate(WidgetsBinding.instance) - ?.platformDispatcher - .onPlatformMessage ?? - ui.channelBuffers.push; - sendMessage( + ui.channelBuffers.push( 'flutter/navigation', _codec.encodeMethodCall( MethodCall('pushRouteInformation', { @@ -107,9 +101,3 @@ Future pushRouteNameToFramework(Object? _, String routeName) { ); return completer.future; } - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml index f8270a15f984..eb897385c0cc 100644 --- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml +++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/url_launcher/ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.1.2 +version: 2.1.3 environment: sdk: ">=2.18.0 <4.0.0" From 5933993ef519b67080d2ff50d184bc81d44dc081 Mon Sep 17 00:00:00 2001 From: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com> Date: Fri, 16 Jun 2023 20:43:49 -0700 Subject: [PATCH 48/53] Add missing ignores for `textScaleFactor` deprecation (#4239) https://github.com/flutter/packages/pull/4209 missed a few deprecations since it was reformatted and the "ignore" was placed on the incorrect lines. Now `dart analyze --fatal-infos` says no issues found. --- packages/flutter_markdown/lib/src/builder.dart | 8 ++++---- .../test/text_scale_factor_test.dart | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/flutter_markdown/lib/src/builder.dart b/packages/flutter_markdown/lib/src/builder.dart index b688427e8cdd..f32ec0a19f03 100644 --- a/packages/flutter_markdown/lib/src/builder.dart +++ b/packages/flutter_markdown/lib/src/builder.dart @@ -829,8 +829,8 @@ class MarkdownBuilder implements md.NodeVisitor { if (selectable) { return SelectableText.rich( text!, - textScaleFactor: - styleSheet.textScaleFactor, // ignore: deprecated_member_use + // ignore: deprecated_member_use + textScaleFactor: styleSheet.textScaleFactor, textAlign: textAlign ?? TextAlign.start, onTap: onTapText, key: k, @@ -838,8 +838,8 @@ class MarkdownBuilder implements md.NodeVisitor { } else { return RichText( text: text!, - textScaleFactor: - styleSheet.textScaleFactor!, // ignore: deprecated_member_use + // ignore: deprecated_member_use + textScaleFactor: styleSheet.textScaleFactor!, textAlign: textAlign ?? TextAlign.start, key: k, ); diff --git a/packages/flutter_markdown/test/text_scale_factor_test.dart b/packages/flutter_markdown/test/text_scale_factor_test.dart index 41b5ec7c9beb..2f3138a94aef 100644 --- a/packages/flutter_markdown/test/text_scale_factor_test.dart +++ b/packages/flutter_markdown/test/text_scale_factor_test.dart @@ -36,8 +36,8 @@ void defineTests() { await tester.pumpWidget( boilerplate( const MediaQuery( - data: MediaQueryData( - textScaleFactor: 2.0), // ignore: deprecated_member_use + // ignore: deprecated_member_use + data: MediaQueryData(textScaleFactor: 2.0), child: MarkdownBody( data: data, ), @@ -57,8 +57,8 @@ void defineTests() { await tester.pumpWidget( boilerplate( const MediaQuery( - data: MediaQueryData( - textScaleFactor: 2.0), // ignore: deprecated_member_use + // ignore: deprecated_member_use + data: MediaQueryData(textScaleFactor: 2.0), child: MarkdownBody( data: data, selectable: true, @@ -69,8 +69,8 @@ void defineTests() { final SelectableText selectableText = tester.widget(find.byType(SelectableText)); - expect(selectableText.textScaleFactor, - 2.0); // ignore: deprecated_member_use + // ignore: deprecated_member_use + expect(selectableText.textScaleFactor, 2.0); }, ); }); From 6e1918fa166c2ff0a0f82c62f3e0c0184941ac92 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Mon, 19 Jun 2023 08:28:10 -0700 Subject: [PATCH 49/53] [all] Update UIViewControllerBasedStatusBarAppearance to true (#4225) Now that we support UIViewControllerBasedStatusBar by default (after engine roll: https://github.com/flutter/flutter/commit/c7167765d74c1980c9ba750c5750ae27817ad630), we should make this value to be true. Part of https://github.com/flutter/flutter/issues/128969 --- packages/animations/example/ios/Runner/Info.plist | 2 -- packages/camera/camera/example/ios/Runner/Info.plist | 2 -- .../camera/camera_avfoundation/example/ios/Runner/Info.plist | 2 -- packages/dynamic_layouts/example/ios/Runner/Info.plist | 2 -- .../example/ios/Runner/Info.plist | 2 -- .../file_selector/file_selector/example/ios/Runner/Info.plist | 2 -- .../file_selector_ios/example/ios/Runner/Info.plist | 2 -- .../flutter_adaptive_scaffold/example/ios/Runner/Info.plist | 2 -- packages/flutter_markdown/example/ios/Runner/Info.plist | 2 -- packages/go_router/example/ios/Runner/Info.plist | 2 -- .../google_maps_flutter/example/ios/Runner/Info.plist | 2 -- .../google_maps_flutter_ios/example/ios11/ios/Runner/Info.plist | 2 -- .../google_maps_flutter_ios/example/ios12/ios/Runner/Info.plist | 2 -- .../google_maps_flutter_ios/example/ios13/ios/Runner/Info.plist | 2 -- .../google_sign_in/google_sign_in/example/ios/Runner/Info.plist | 2 -- .../google_sign_in_ios/example/ios/Runner/Info.plist | 2 -- .../image_picker/image_picker/example/ios/Runner/Info.plist | 2 -- .../image_picker/image_picker_ios/example/ios/Runner/Info.plist | 2 -- .../in_app_purchase/example/ios/Runner/Info.plist | 2 -- .../in_app_purchase_storekit/example/ios/Runner/Info.plist | 2 -- packages/ios_platform_images/example/ios/Runner/Info.plist | 2 -- packages/local_auth/local_auth/example/ios/Runner/Info.plist | 2 -- .../local_auth/local_auth_ios/example/ios/Runner/Info.plist | 2 -- packages/palette_generator/example/ios/Runner/Info.plist | 2 -- .../path_provider/path_provider/example/ios/Runner/Info.plist | 2 -- .../path_provider_foundation/example/ios/Runner/Info.plist | 2 -- packages/pigeon/example/app/ios/Runner/Info.plist | 2 -- .../example/ios/Runner/Info.plist | 2 -- .../platform_tests/test_plugin/example/ios/Runner/Info.plist | 2 -- .../quick_actions/quick_actions/example/ios/Runner/Info.plist | 2 -- .../quick_actions_ios/example/ios/Runner/Info.plist | 2 -- packages/rfw/example/hello/ios/Runner/Info.plist | 2 -- packages/rfw/example/local/ios/Runner/Info.plist | 2 -- packages/rfw/example/remote/ios/Runner/Info.plist | 2 -- .../shared_preferences/example/ios/Runner/Info.plist | 2 -- .../shared_preferences_foundation/example/ios/Runner/Info.plist | 2 -- .../url_launcher/url_launcher/example/ios/Runner/Info.plist | 2 -- .../url_launcher/url_launcher_ios/example/ios/Runner/Info.plist | 2 -- .../video_player/video_player/example/ios/Runner/Info.plist | 2 -- .../video_player_avfoundation/example/ios/Runner/Info.plist | 2 -- .../webview_flutter/example/ios/Runner/Info.plist | 2 -- .../webview_flutter_wkwebview/example/ios/Runner/Info.plist | 2 -- 42 files changed, 84 deletions(-) diff --git a/packages/animations/example/ios/Runner/Info.plist b/packages/animations/example/ios/Runner/Info.plist index a060db61e461..1251b459385b 100644 --- a/packages/animations/example/ios/Runner/Info.plist +++ b/packages/animations/example/ios/Runner/Info.plist @@ -39,7 +39,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - diff --git a/packages/camera/camera/example/ios/Runner/Info.plist b/packages/camera/camera/example/ios/Runner/Info.plist index bacd9e54f1ea..13e41200aca1 100644 --- a/packages/camera/camera/example/ios/Runner/Info.plist +++ b/packages/camera/camera/example/ios/Runner/Info.plist @@ -50,8 +50,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist b/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist index ca93baac7012..adb62fb7803d 100644 --- a/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist +++ b/packages/camera/camera_avfoundation/example/ios/Runner/Info.plist @@ -52,8 +52,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/dynamic_layouts/example/ios/Runner/Info.plist b/packages/dynamic_layouts/example/ios/Runner/Info.plist index 7f553465b77e..5458fc4188bf 100644 --- a/packages/dynamic_layouts/example/ios/Runner/Info.plist +++ b/packages/dynamic_layouts/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist index aa6d84f63af1..28ab78e39981 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/file_selector/file_selector/example/ios/Runner/Info.plist b/packages/file_selector/file_selector/example/ios/Runner/Info.plist index 7f553465b77e..5458fc4188bf 100644 --- a/packages/file_selector/file_selector/example/ios/Runner/Info.plist +++ b/packages/file_selector/file_selector/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner/Info.plist b/packages/file_selector/file_selector_ios/example/ios/Runner/Info.plist index 2bf6e923d3b6..67d621a7fc80 100644 --- a/packages/file_selector/file_selector_ios/example/ios/Runner/Info.plist +++ b/packages/file_selector/file_selector_ios/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/flutter_adaptive_scaffold/example/ios/Runner/Info.plist b/packages/flutter_adaptive_scaffold/example/ios/Runner/Info.plist index 7f553465b77e..5458fc4188bf 100644 --- a/packages/flutter_adaptive_scaffold/example/ios/Runner/Info.plist +++ b/packages/flutter_adaptive_scaffold/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/flutter_markdown/example/ios/Runner/Info.plist b/packages/flutter_markdown/example/ios/Runner/Info.plist index 6c83e4da6515..a5b939285a5a 100644 --- a/packages/flutter_markdown/example/ios/Runner/Info.plist +++ b/packages/flutter_markdown/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/go_router/example/ios/Runner/Info.plist b/packages/go_router/example/ios/Runner/Info.plist index 4f68a2cee180..677cf7bb8b0e 100644 --- a/packages/go_router/example/ios/Runner/Info.plist +++ b/packages/go_router/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist index d6b389f16721..6783ca935f1d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist @@ -45,8 +45,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Info.plist index d6b389f16721..6783ca935f1d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Info.plist +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios11/ios/Runner/Info.plist @@ -45,8 +45,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Info.plist index d6b389f16721..6783ca935f1d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Info.plist +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios12/ios/Runner/Info.plist @@ -45,8 +45,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Info.plist index d6b389f16721..6783ca935f1d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Info.plist +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/ios13/ios/Runner/Info.plist @@ -45,8 +45,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Info.plist b/packages/google_sign_in/google_sign_in/example/ios/Runner/Info.plist index 6c749634f53d..08fef9a9fe42 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Runner/Info.plist +++ b/packages/google_sign_in/google_sign_in/example/ios/Runner/Info.plist @@ -56,8 +56,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist index 6c749634f53d..08fef9a9fe42 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner/Info.plist @@ -56,8 +56,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/image_picker/image_picker/example/ios/Runner/Info.plist b/packages/image_picker/image_picker/example/ios/Runner/Info.plist index 423e21fd1672..90eb79e9ad18 100755 --- a/packages/image_picker/image_picker/example/ios/Runner/Info.plist +++ b/packages/image_picker/image_picker/example/ios/Runner/Info.plist @@ -53,8 +53,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist b/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist index 423e21fd1672..90eb79e9ad18 100755 --- a/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist +++ b/packages/image_picker/image_picker_ios/example/ios/Runner/Info.plist @@ -53,8 +53,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Info.plist b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Info.plist index 3c493732947a..5304b6ed1f84 100644 --- a/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Info.plist +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist index 3c493732947a..5304b6ed1f84 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/ios_platform_images/example/ios/Runner/Info.plist b/packages/ios_platform_images/example/ios/Runner/Info.plist index bebb28ae7cf0..c454edd1f86e 100644 --- a/packages/ios_platform_images/example/ios/Runner/Info.plist +++ b/packages/ios_platform_images/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/local_auth/local_auth/example/ios/Runner/Info.plist b/packages/local_auth/local_auth/example/ios/Runner/Info.plist index 1af663b3f83c..2dc92f5dff1c 100644 --- a/packages/local_auth/local_auth/example/ios/Runner/Info.plist +++ b/packages/local_auth/local_auth/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - NSFaceIDUsageDescription App needs to authenticate using faces. CADisableMinimumFrameDurationOnPhone diff --git a/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist b/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist index 1af663b3f83c..2dc92f5dff1c 100644 --- a/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist +++ b/packages/local_auth/local_auth_ios/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - NSFaceIDUsageDescription App needs to authenticate using faces. CADisableMinimumFrameDurationOnPhone diff --git a/packages/palette_generator/example/ios/Runner/Info.plist b/packages/palette_generator/example/ios/Runner/Info.plist index cdf4f69014e5..fb29f390fe3b 100644 --- a/packages/palette_generator/example/ios/Runner/Info.plist +++ b/packages/palette_generator/example/ios/Runner/Info.plist @@ -39,7 +39,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - diff --git a/packages/path_provider/path_provider/example/ios/Runner/Info.plist b/packages/path_provider/path_provider/example/ios/Runner/Info.plist index 150b4d3f2dc7..122243319e2b 100644 --- a/packages/path_provider/path_provider/example/ios/Runner/Info.plist +++ b/packages/path_provider/path_provider/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/path_provider/path_provider_foundation/example/ios/Runner/Info.plist b/packages/path_provider/path_provider_foundation/example/ios/Runner/Info.plist index 5bdb9bcc0635..45ccb93dc4d5 100644 --- a/packages/path_provider/path_provider_foundation/example/ios/Runner/Info.plist +++ b/packages/path_provider/path_provider_foundation/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/pigeon/example/app/ios/Runner/Info.plist b/packages/pigeon/example/app/ios/Runner/Info.plist index 6bca58717f45..b6439ae077fb 100644 --- a/packages/pigeon/example/app/ios/Runner/Info.plist +++ b/packages/pigeon/example/app/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/pigeon/platform_tests/alternate_language_test_plugin/example/ios/Runner/Info.plist b/packages/pigeon/platform_tests/alternate_language_test_plugin/example/ios/Runner/Info.plist index 5520fc125c4d..08ea2be3c97b 100644 --- a/packages/pigeon/platform_tests/alternate_language_test_plugin/example/ios/Runner/Info.plist +++ b/packages/pigeon/platform_tests/alternate_language_test_plugin/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/pigeon/platform_tests/test_plugin/example/ios/Runner/Info.plist b/packages/pigeon/platform_tests/test_plugin/example/ios/Runner/Info.plist index 192997d10b4d..ccd141f08045 100644 --- a/packages/pigeon/platform_tests/test_plugin/example/ios/Runner/Info.plist +++ b/packages/pigeon/platform_tests/test_plugin/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/quick_actions/quick_actions/example/ios/Runner/Info.plist b/packages/quick_actions/quick_actions/example/ios/Runner/Info.plist index 2128c14bb939..f572a5d0dfda 100644 --- a/packages/quick_actions/quick_actions/example/ios/Runner/Info.plist +++ b/packages/quick_actions/quick_actions/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist index 2128c14bb939..f572a5d0dfda 100644 --- a/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist +++ b/packages/quick_actions/quick_actions_ios/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/rfw/example/hello/ios/Runner/Info.plist b/packages/rfw/example/hello/ios/Runner/Info.plist index 2f6dda1e835f..8d1d37dff8d6 100644 --- a/packages/rfw/example/hello/ios/Runner/Info.plist +++ b/packages/rfw/example/hello/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/rfw/example/local/ios/Runner/Info.plist b/packages/rfw/example/local/ios/Runner/Info.plist index 031ce64cd1e8..675f7bfd9240 100644 --- a/packages/rfw/example/local/ios/Runner/Info.plist +++ b/packages/rfw/example/local/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/rfw/example/remote/ios/Runner/Info.plist b/packages/rfw/example/remote/ios/Runner/Info.plist index c11cb1e95dc8..6b4c897e78ca 100644 --- a/packages/rfw/example/remote/ios/Runner/Info.plist +++ b/packages/rfw/example/remote/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner/Info.plist b/packages/shared_preferences/shared_preferences/example/ios/Runner/Info.plist index 99a4c9290737..12a4ff084cbc 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Runner/Info.plist +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Info.plist b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Info.plist index 30d5f4b0e845..6d2946cb3ea5 100644 --- a/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Info.plist +++ b/packages/shared_preferences/shared_preferences_foundation/example/ios/Runner/Info.plist @@ -41,8 +41,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/url_launcher/url_launcher/example/ios/Runner/Info.plist b/packages/url_launcher/url_launcher/example/ios/Runner/Info.plist index 7d28adf648b2..be02cc9d4bf2 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner/Info.plist +++ b/packages/url_launcher/url_launcher/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Info.plist b/packages/url_launcher/url_launcher_ios/example/ios/Runner/Info.plist index 7d28adf648b2..be02cc9d4bf2 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner/Info.plist +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner/Info.plist @@ -43,8 +43,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/video_player/video_player/example/ios/Runner/Info.plist b/packages/video_player/video_player/example/ios/Runner/Info.plist index 74d07293aa9e..4e29652e6d2e 100644 --- a/packages/video_player/video_player/example/ios/Runner/Info.plist +++ b/packages/video_player/video_player/example/ios/Runner/Info.plist @@ -48,8 +48,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist b/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist index 74d07293aa9e..4e29652e6d2e 100644 --- a/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist +++ b/packages/video_player/video_player_avfoundation/example/ios/Runner/Info.plist @@ -48,8 +48,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist index ee17126a336f..6e0d80c22b91 100644 --- a/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist index 1b07552b898d..6aa6702a86e1 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist @@ -39,8 +39,6 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - CADisableMinimumFrameDurationOnPhone UIApplicationSupportsIndirectInputEvents From 418aa3da0c97699af2a651e7344cd190f2201db7 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 20 Jun 2023 12:27:17 -0400 Subject: [PATCH 50/53] [ci] Switch Linux and web build-all to LUCI (#4252) Enables the recently added LUCI build-all for Linux and web, and removes the Cirrus version. Also enables an iOS build-all that was never enabled. Part of https://github.com/flutter/flutter/issues/114373 --- .ci.yaml | 7 ++----- .cirrus.yml | 15 --------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/.ci.yaml b/.ci.yaml index 8dc53d81b7cf..620432aca174 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -81,16 +81,15 @@ targets: ### Web tasks ### - name: Linux_web web_build_all_packages master - bringup: true # New target recipe: packages/packages timeout: 30 properties: + add_recipes_cq: "true" version_file: flutter_master.version target_file: web_build_all_packages.yaml channel: master - name: Linux_web web_build_all_packages stable - bringup: true # New target recipe: packages/packages timeout: 30 properties: @@ -100,16 +99,15 @@ targets: ### Linux desktop tasks - name: Linux_desktop build_all_packages master - bringup: true # New target recipe: packages/packages timeout: 30 properties: + add_recipes_cq: "true" version_file: flutter_master.version target_file: linux_build_all_packages.yaml channel: master - name: Linux_desktop build_all_packages stable - bringup: true # New target recipe: packages/packages timeout: 30 properties: @@ -203,7 +201,6 @@ targets: target_file: ios_build_all_packages.yaml - name: Mac_x64 ios_build_all_packages stable - bringup: true # New target recipe: packages/packages timeout: 30 properties: diff --git a/.cirrus.yml b/.cirrus.yml index d7ee3cc6ff00..0808a1cabb52 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -176,22 +176,7 @@ task: env: CIRRUS_CLONE_SUBMODULES: true script: ./script/tool_runner.sh update-excerpts --fail-on-change - ### Web tasks ### - - name: web-build_all_packages - env: - BUILD_ALL_ARGS: "web" - matrix: - CHANNEL: "master" - CHANNEL: "stable" - << : *BUILD_ALL_PACKAGES_APP_TEMPLATE ### Linux desktop tasks ### - - name: linux-build_all_packages - env: - BUILD_ALL_ARGS: "linux" - matrix: - CHANNEL: "master" - CHANNEL: "stable" - << : *BUILD_ALL_PACKAGES_APP_TEMPLATE - name: linux-platform_tests # Don't run full platform tests on both channels in pre-submit. skip: $CIRRUS_PR != '' && $CHANNEL == 'stable' From 700c40ae24b120cfbe1113e290e63862cc2dac48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 21:36:12 +0000 Subject: [PATCH 51/53] [in_app_pur]: Bump org.jetbrains.kotlin:kotlin-bom from 1.8.21 to 1.8.22 in /packages/in_app_purchase/in_app_purchase_android/android (#4193) Bumps [org.jetbrains.kotlin:kotlin-bom](https://github.com/JetBrains/kotlin) from 1.8.21 to 1.8.22.
Release notes

Sourced from org.jetbrains.kotlin:kotlin-bom's releases.

Kotlin 1.8.22

Changelog

Tools. Gradle

  • KT-58280 org.jetbrains.kotlin.jvm Gradle plugin contributes build directories to the test compile classpath

Checksums

File Sha256
kotlin-compiler-1.8.22.zip 91f50fe25c9edfb1e79ae1fe2ede85fa4728f7f4b0587644a4eee40252cdfaa6
kotlin-native-linux-x86_64-1.8.22.tar.gz a1fb41fc010b347d5d9a5449ebb48ad200c59ec2a9121b01db5165db6697e58b
kotlin-native-macos-x86_64-1.8.22.tar.gz 0d6e6b12569a4b8ff2f301f827192dd715a29962cc01eed05557aa8e6eb7c20d
kotlin-native-macos-aarch64-1.8.22.tar.gz 29805af3220eab3c163ac54f02a6097436d4ddfa83eca7815eb053517093e417
kotlin-native-windows-x86_64-1.8.22.zip 91b04aa9f3dc3d5968c75d8e7f163e542458867915777e995162864cc805b2e5
Changelog

Sourced from org.jetbrains.kotlin:kotlin-bom's changelog.

1.8.22

Tools. Gradle

  • KT-58280 org.jetbrains.kotlin.jvm Gradle plugin contributes build directories to the test compile classpath
Commits
  • fe01561 Add changelog for 1.8.22
  • 918a74f [Gradle] KotlinCompilationAssociator: Restore 1.8.10 behaviour for KotlinWith...
  • 90a9bf9 [Gradle] Implement KT58280JvmWithJavaTestCompileClasspath to cover KT-58280
  • 0043ae4 Update Dokka to 1.8.20-dev-213
  • 0389c35 docs build: allow to specify custom dokka repository
  • d57f335 docs build: move parameter initialization to the parent project
  • 596ff23 docs: cleanup remaining previous version specializations
  • 2da94ed Fix missing native-wasm source set in legacy docs build
  • 51d55cd docs: specialize build for latest version
  • 5e1e4ec Render kotlin-reflect library documentation in a separate module
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=org.jetbrains.kotlin:kotlin-bom&package-manager=gradle&previous-version=1.8.21&new-version=1.8.22)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md | 4 ++++ .../in_app_purchase_android/android/build.gradle | 2 +- packages/in_app_purchase/in_app_purchase_android/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index f32df46b48dd..0ed82c7e10fc 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.0+6 + +* Bumps org.jetbrains.kotlin:kotlin-bom from 1.8.21 to 1.8.22. + ## 0.3.0+5 * Bumps org.jetbrains.kotlin:kotlin-bom from 1.8.0 to 1.8.21. diff --git a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle index 3a2344bda3ee..81d69a40cce2 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle +++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle @@ -60,7 +60,7 @@ dependencies { implementation 'androidx.annotation:annotation:1.6.0' // org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions. // See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7 - implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.21")) + implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22")) implementation 'com.android.billingclient:billing:5.2.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.json:json:20230227' diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index 2132e84abba2..d8ac5f6d38c2 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,7 +2,7 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/packages/tree/main/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.3.0+5 +version: 0.3.0+6 environment: sdk: ">=2.18.0 <4.0.0" From 20590bfccc0df04292b261ba698325094929fcfe Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Tue, 20 Jun 2023 17:41:11 -0400 Subject: [PATCH 52/53] [ci] Add LUCI version of the analyze tasks (#4253) Add LUCI versions of the analyze, pathified analyze, and downgraded analayze tasks. Does not include legacy analyze (N-1 and N-2 stable Flutter versions) since there are open questions about how to structure those in LUCI. Part of https://github.com/flutter/flutter/issues/114373 --- .ci.yaml | 38 ++++++++++++++++++++++++++++- .ci/scripts/analyze_repo_tools.sh | 8 ++++++ .ci/scripts/pathified_analyze.sh | 17 +++++++++++++ .ci/targets/analyze.yaml | 15 ++++++++++++ .ci/targets/analyze_downgraded.yaml | 10 ++++++++ 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100755 .ci/scripts/analyze_repo_tools.sh create mode 100755 .ci/scripts/pathified_analyze.sh create mode 100644 .ci/targets/analyze.yaml create mode 100644 .ci/targets/analyze_downgraded.yaml diff --git a/.ci.yaml b/.ci.yaml index 620432aca174..275a8f1a179e 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -69,7 +69,7 @@ platform_properties: } targets: - ### Linux-host tasks ### + ### Linux-host general tasks ### - name: Linux repo_tools_tests recipe: packages/packages timeout: 30 @@ -79,6 +79,42 @@ targets: channel: master version_file: flutter_master.version + - name: Linux analyze master + bringup: true # New target + recipe: packages/packages + timeout: 30 + properties: + target_file: analyze.yaml + channel: master + version_file: flutter_master.version + + - name: Linux analyze stable + bringup: true # New target + recipe: packages/packages + timeout: 30 + properties: + target_file: analyze.yaml + channel: stable + version_file: flutter_stable.version + + - name: Linux analyze_downgraded master + bringup: true # New target + recipe: packages/packages + timeout: 30 + properties: + target_file: analyze_downgraded.yaml + channel: master + version_file: flutter_master.version + + - name: Linux analyze_downgraded stable + bringup: true # New target + recipe: packages/packages + timeout: 30 + properties: + target_file: analyze_downgraded.yaml + channel: stable + version_file: flutter_stable.version + ### Web tasks ### - name: Linux_web web_build_all_packages master recipe: packages/packages diff --git a/.ci/scripts/analyze_repo_tools.sh b/.ci/scripts/analyze_repo_tools.sh new file mode 100755 index 000000000000..df2a87c04a98 --- /dev/null +++ b/.ci/scripts/analyze_repo_tools.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +set -e + +cd script/tool +dart analyze --fatal-infos diff --git a/.ci/scripts/pathified_analyze.sh b/.ci/scripts/pathified_analyze.sh new file mode 100755 index 000000000000..2942cf7d57b8 --- /dev/null +++ b/.ci/scripts/pathified_analyze.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +set -e + +# Pathify the dependencies on changed packages (excluding major version +# changes, which won't affect clients). +./script/tool_runner.sh make-deps-path-based --target-dependencies-with-non-breaking-updates +# This uses --run-on-dirty-packages rather than --packages-for-branch +# since only the packages changed by 'make-deps-path-based' need to be +# re-checked. +dart ./script/tool/bin/flutter_plugin_tools.dart analyze --run-on-dirty-packages \ + --log-timing --custom-analysis=script/configs/custom_analysis.yaml +# Restore the tree to a clean state, to avoid accidental issues if +# other script steps are added to the enclosing task. +git checkout . diff --git a/.ci/targets/analyze.yaml b/.ci/targets/analyze.yaml new file mode 100644 index 000000000000..25791d3c898c --- /dev/null +++ b/.ci/targets/analyze.yaml @@ -0,0 +1,15 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: analyze repo tools + script: .ci/scripts/analyze_repo_tools.sh + - name: analyze + script: script/tool_runner.sh + # DO NOT change the custom-analysis argument here without changing the Dart repo. + # See the comment in script/configs/custom_analysis.yaml for details. + args: ["analyze", "--custom-analysis=script/configs/custom_analysis.yaml"] + # Re-run analysis with path-based dependencies to ensure that publishing + # the changes won't break analysis of other packages in the respository + # that depend on it. + - name: analyze - pathified + script: .ci/scripts/pathified_analyze.sh diff --git a/.ci/targets/analyze_downgraded.yaml b/.ci/targets/analyze_downgraded.yaml new file mode 100644 index 000000000000..46bc111b30c5 --- /dev/null +++ b/.ci/targets/analyze_downgraded.yaml @@ -0,0 +1,10 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + # Does a sanity check that packages pass analysis with the lowest possible + # versions of all dependencies. This is to catch cases where we add use of + # new APIs but forget to update minimum versions of dependencies to where + # those APIs are introduced. + - name: analyze - downgraded + script: script/tool_runner.sh + args: ["analyze", "--downgrade", "--custom-analysis=script/configs/custom_analysis.yaml"] From c996143beb2de6e1753ef8a4f0f7af78c48c3a12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 23:35:43 +0000 Subject: [PATCH 53/53] Bump actions/labeler from 4.0.3 to 4.1.0 (#4145) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/labeler](https://github.com/actions/labeler) from 4.0.3 to 4.1.0.
Release notes

Sourced from actions/labeler's releases.

v4.1.0

What's Changed

In scope of this release, the dot input was added by @​kachkaev in actions/labeler#316. It allows patterns to match paths starting with a period. This input is set to false by default.

Usage

name: "Pull Request Labeler"
on:
- pull_request_target

jobs: triage: permissions: contents: read pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/labeler@v4 with: dot: true

This release also includes the following changes:

New Contributors

Full Changelog: https://github.com/actions/labeler/compare/v4...v4.1.0

v4.0.4

What's Changed

New Contributors

... (truncated)

Commits
  • 9fcb2c2 Merge pull request #578 from actions/dependabot/npm_and_yarn/typescript-eslin...
  • 0d06c50 Bump @​typescript-eslint/eslint-plugin from 5.59.7 to 5.59.8
  • 1d399c3 Merge pull request #577 from actions/dependabot/npm_and_yarn/typescript-eslin...
  • 82a4f6f Merge pull request #316 from kachkaev/dot-option
  • d40596e micromatch → minimatch
  • 3cbc54c Merge pull request #451 from Youssef1313/patch-1
  • 639ba81 Rebuild
  • 71d2484 Address review comment
  • 59d3310 Rebuild
  • a78a6c7 Update README.md
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/labeler&package-manager=github_actions&previous-version=4.0.3&new-version=4.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- .github/workflows/pull_request_label.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml index e48c050fb88d..a474d2c4b5e4 100644 --- a/.github/workflows/pull_request_label.yml +++ b/.github/workflows/pull_request_label.yml @@ -21,7 +21,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@ba790c862c380240c6d5e7427be5ace9a05c754b + - uses: actions/labeler@9fcb2c2f5584144ca754f8bfe8c6f81e77753375 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true