diff --git a/.cirrus.yml b/.cirrus.yml index 98cd6276e0e6..4ec73ea3f24c 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -39,8 +39,16 @@ task: - flutter channel $CHANNEL - ./script/incremental_build.sh test - name: analyze + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" script: ./script/incremental_build.sh analyze - name: build_all_plugins_apk + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" script: # TODO(jackson): Allow web plugins once supported on stable # https://github.com/flutter/flutter/issues/42864 @@ -132,6 +140,7 @@ task: osx_instance: image: catalina-xcode-11.3.1-flutter upgrade_script: + - sudo gem install cocoapods - flutter channel stable - flutter upgrade - flutter channel master @@ -143,6 +152,10 @@ task: - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot matrix: - name: build_all_plugins_ipa + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" script: # TODO(jackson): Allow web plugins once supported on stable # https://github.com/flutter/flutter/issues/42864 @@ -190,6 +203,7 @@ task: setup_script: - flutter config --enable-macos-desktop upgrade_script: + - sudo gem install cocoapods - flutter channel master - flutter upgrade - git fetch origin master diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 083e125b32d2..741216982d35 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,37 +1,32 @@ -## Description +*Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* -*Replace this paragraph with a description of what this PR is doing. If you're modifying existing behavior, describe the existing behavior, how this PR is changing it, and what motivated the change.* +*List which issues are fixed by this PR. You must list at least one issue.* -## Related Issues +*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* -*Replace this paragraph with a list of issues related to this PR from the [issue database](https://github.com/flutter/flutter/issues). Indicate, which of these issues are resolved or fixed by this PR. Note that you'll have to prefix the issue numbers with flutter/flutter#.* - -## Checklist - -Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (`[x]`). This will ensure a smooth and quick review process. +## Pre-launch Checklist +- [ ] The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. `[shared_preferences]` - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. -- [ ] My PR includes unit or integration tests for *all* changed/updated/fixed behaviors (See [Contributor Guide]). -- [ ] All existing and new tests are passing. -- [ ] I updated/added relevant documentation (doc comments with `///`). -- [ ] The analyzer (`flutter analyze`) does not report any problems on my PR. -- [ ] I read and followed the [Flutter Style Guide]. -- [ ] The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. [shared_preferences] +- [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. +- [ ] I read and followed the [Flutter Style Guide] and the [C++, Objective-C, Java style guides]. +- [ ] I listed at least one issue that this PR fixes in the description above. +- [ ] I added new tests to check the change I am making or feature I am adding, or Hixie said the PR is test exempt. - [ ] I updated pubspec.yaml with an appropriate new version according to the [pub versioning philosophy]. - [ ] I updated CHANGELOG.md to add a description of the change. +- [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I signed the [CLA]. -- [ ] I am willing to follow-up on review comments in a timely manner. - -## Breaking Change - -Does your PR require plugin users to manually update their apps to accommodate your change? +- [ ] All existing and new tests are passing. -- [ ] Yes, this is a breaking change (please indicate a breaking change in CHANGELOG.md and increment major revision). -- [ ] No, this is *not* a breaking change. +If you need help, consider asking for advice on the #hackers-new channel on [Discord]. -[issue database]: https://github.com/flutter/flutter/issues -[Contributor Guide]: https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md +[Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview +[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo -[pub versioning philosophy]: https://www.dartlang.org/tools/pub/versioning +[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/master/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 diff --git a/AUTHORS b/AUTHORS index 51345c9a3481..09bab9d34d02 100644 --- a/AUTHORS +++ b/AUTHORS @@ -59,3 +59,4 @@ Kazuki Yamaguchi Eitan Schwartz Chris Rutkowski Juan Alvarez +Aleksandr Yurkovskiy diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b278b43e524f..b763320b67c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ [![Build Status](https://api.cirrus-ci.com/github/flutter/plugins.svg)](https://cirrus-ci.com/github/flutter/plugins/master) -_See also: [Flutter's code of conduct](https://flutter.io/design-principles/#code-of-conduct)_ +_See also: [Flutter's code of conduct](https://github.com/flutter/flutter/blob/master/CODE_OF_CONDUCT.md)_ ## Things you will need @@ -131,9 +131,8 @@ pub global run flutter_plugin_tools xctest --target RunnerUITests --skip ` * Hack away. - * Verify changes with [flutter_plugin_tools](https://pub.dartlang.org/packages/flutter_plugin_tools) + * Verify changes with [flutter_plugin_tools](https://pub.dev/packages/flutter_plugin_tools) ``` pub global activate flutter_plugin_tools pub global run flutter_plugin_tools format --plugins plugin_name diff --git a/analysis_options.yaml b/analysis_options.yaml index b1261f36fac9..47cdbd2f98dc 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,6 +4,9 @@ analyzer: # Ignore generated files - '**/*.g.dart' - 'lib/src/generated/*.dart' + errors: + always_require_non_null_named_parameters: false # not needed with nnbd + unnecessary_null_comparison: false # Turned as long as nnbd mix-mode is supported. linter: rules: - public_member_api_docs diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index 023a140fbcc9..df5b7a2fa752 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.4.5+20 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.4.5+19 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.4.5+18 + +* Update Flutter SDK constraint. + ## 0.4.5+17 * Update Dart SDK constraint in example. diff --git a/packages/android_alarm_manager/README.md b/packages/android_alarm_manager/README.md index e7c7f6ee2713..cf02bf66ff11 100644 --- a/packages/android_alarm_manager/README.md +++ b/packages/android_alarm_manager/README.md @@ -1,6 +1,6 @@ # android_alarm_manager -[![pub package](https://img.shields.io/pub/v/android_alarm_manager.svg)](https://pub.dartlang.org/packages/android_alarm_manager) +[![pub package](https://img.shields.io/pub/v/android_alarm_manager.svg)](https://pub.dev/packages/android_alarm_manager) A Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire. @@ -36,7 +36,7 @@ Next, within the `` tags, add: android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver" android:enabled="false"> - + @@ -121,6 +121,6 @@ register plugins. This can be resolved by running `flutter upgrade` to upgrade to the latest Flutter version.** For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml b/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml index 356c10f45651..2a9dc331ebf1 100644 --- a/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml +++ b/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml @@ -41,7 +41,7 @@ android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver" android:enabled="false"> - + { ), ], ), - RaisedButton( + ElevatedButton( child: Text( 'Schedule OneShot Alarm', ), diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml index 9de2d8308419..d1771c0f0380 100644 --- a/packages/android_alarm_manager/pubspec.yaml +++ b/packages/android_alarm_manager/pubspec.yaml @@ -4,7 +4,7 @@ description: Flutter plugin for accessing the Android AlarmManager service, and # 0.4.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.4.5+17 +version: 0.4.5+20 homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager dependencies: @@ -25,4 +25,4 @@ flutter: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index 5a3ba03b33eb..3e878a50aac2 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.0.0-nullsafety.2 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 2.0.0-nullsafety.1 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 2.0.0-nullsafety + +* Migrate to null safety. + +## 0.3.7+8 + +* Update Flutter SDK constraint. + ## 0.3.7+7 * Update Dart SDK constraint in example. diff --git a/packages/android_intent/README.md b/packages/android_intent/README.md index f7dfdfed9860..aabf059ea336 100644 --- a/packages/android_intent/README.md +++ b/packages/android_intent/README.md @@ -53,13 +53,13 @@ if (platform.isAndroid) { Feel free to add support for additional Android intents. The Dart values supported for the arguments parameter, and their corresponding -Android values, are listed [here](https://flutter.io/platform-channels/#codec). +Android values, are listed [here](https://flutter.dev/docs/development/platform-integration/platform-channels#codec). On the Android side, the arguments are used to populate an Android `Bundle` instance. This process currently restricts the use of lists to homogeneous lists of integers or strings. > Note that a similar method does not currently exist for iOS. Instead, the -[url_launcher](https://pub.dartlang.org/packages/url_launcher) plugin +[url_launcher](https://pub.dev/packages/url_launcher) plugin can be used for deep linking. Url launcher can also be used for creating ACTION_VIEW intents for Android, however this intent plugin also allows clients to set extra parameters for the intent. @@ -67,6 +67,6 @@ clients to set extra parameters for the intent. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/android_intent/example/lib/main.dart b/packages/android_intent/example/lib/main.dart index 45de9632e975..4fd2440285f3 100644 --- a/packages/android_intent/example/lib/main.dart +++ b/packages/android_intent/example/lib/main.dart @@ -59,12 +59,12 @@ class MyHomePage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - RaisedButton( + ElevatedButton( child: const Text( 'Tap here to set an alarm\non weekdays at 9:30pm.'), onPressed: _createAlarm, ), - RaisedButton( + ElevatedButton( child: const Text('Tap here to test explicit intents.'), onPressed: () => _openExplicitIntentsView(context)), ], @@ -166,40 +166,40 @@ class ExplicitIntentsWidget extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - RaisedButton( + ElevatedButton( child: const Text( 'Tap here to display panorama\nimagery in Google Street View.'), onPressed: _openGoogleMapsStreetView, ), - RaisedButton( + ElevatedButton( child: const Text('Tap here to display\na map in Google Maps.'), onPressed: _displayMapInGoogleMaps, ), - RaisedButton( + ElevatedButton( child: const Text( 'Tap here to launch turn-by-turn\nnavigation in Google Maps.'), onPressed: _launchTurnByTurnNavigationInGoogleMaps, ), - RaisedButton( + ElevatedButton( child: const Text('Tap here to open link in Google Chrome.'), onPressed: _openLinkInGoogleChrome, ), - RaisedButton( + ElevatedButton( child: const Text('Tap here to start activity in new task.'), onPressed: _startActivityInNewTask, ), - RaisedButton( + ElevatedButton( child: const Text( 'Tap here to test explicit intent fallback to implicit.'), onPressed: _testExplicitIntentFallback, ), - RaisedButton( + ElevatedButton( child: const Text( 'Tap here to open Location Settings Configuration', ), onPressed: _openLocationSettingsConfiguration, ), - RaisedButton( + ElevatedButton( child: const Text( 'Tap here to open Application Details', ), diff --git a/packages/android_intent/lib/android_intent.dart b/packages/android_intent/lib/android_intent.dart index 9d701979b392..0ab2d7bee420 100644 --- a/packages/android_intent/lib/android_intent.dart +++ b/packages/android_intent/lib/android_intent.dart @@ -36,7 +36,7 @@ class AndroidIntent { this.arguments, this.package, this.componentName, - Platform platform, + Platform? platform, this.type, }) : assert(action != null || componentName != null, 'action or component (or both) must be specified'), @@ -47,8 +47,8 @@ class AndroidIntent { /// app code, it may break without warning. @visibleForTesting AndroidIntent.private({ - @required Platform platform, - @required MethodChannel channel, + required Platform platform, + required MethodChannel channel, this.action, this.flags, this.category, @@ -66,47 +66,47 @@ class AndroidIntent { /// includes constants like `ACTION_VIEW`. /// /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final String action; + final String? action; /// Constants that can be set on an intent to tweak how it is finally handled. /// Some of the constants are mirrored to Dart via [Flag]. /// /// See https://developer.android.com/reference/android/content/Intent.html#setFlags(int). - final List flags; + final List? flags; /// An optional additional constant qualifying the given [action]. /// /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final String category; + final String? category; /// The Uri that the [action] is pointed towards. /// /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final String data; + final String? data; /// The equivalent of `extras`, a generic `Bundle` of data that the Intent can /// carry. This is a slot for extraneous data that the listener may use. /// /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final Map arguments; + final Map? arguments; /// Sets the [data] to only resolve within this given package. /// /// See https://developer.android.com/reference/android/content/Intent.html#setPackage(java.lang.String). - final String package; + final String? package; /// Set the exact `ComponentName` that should handle the intent. If this is /// set [package] should also be non-null. /// /// See https://developer.android.com/reference/android/content/Intent.html#setComponent(android.content.ComponentName). - final String componentName; + final String? componentName; final MethodChannel _channel; final Platform _platform; /// Set an explicit MIME data type. /// /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final String type; + final String? type; bool _isPowerOfTwo(int x) { /* First x in the below expression is for the case when x is 0 */ @@ -146,17 +146,18 @@ class AndroidIntent { return false; } - return await _channel.invokeMethod( + final result = await _channel.invokeMethod( 'canResolveActivity', _buildArguments(), ); + return result!; } /// Constructs the map of arguments which is passed to the plugin. Map _buildArguments() { return { if (action != null) 'action': action, - if (flags != null) 'flags': convertFlags(flags), + if (flags != null) 'flags': convertFlags(flags!), if (category != null) 'category': category, if (data != null) 'data': data, if (arguments != null) 'arguments': arguments, diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index 8d41d5165c68..c61460718fc1 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -1,10 +1,7 @@ name: android_intent description: Flutter plugin for launching Android Intents. Not supported on iOS. homepage: https://github.com/flutter/plugins/tree/master/packages/android_intent -# 0.3.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.3.7+7 +version: 2.0.0-nullsafety.2 flutter: plugin: @@ -16,15 +13,15 @@ flutter: dependencies: flutter: sdk: flutter - platform: ">=2.0.0 <4.0.0" - meta: ^1.0.5 + platform: ^3.0.0-nullsafety.4 + meta: ^1.3.0-nullsafety.6 dev_dependencies: - test: ^1.3.0 - mockito: ^3.0.0 + test: ^1.16.0-nullsafety.13 + mockito: ^4.1.3 flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 environment: - sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/android_intent/test/android_intent_test.dart b/packages/android_intent/test/android_intent_test.dart index 311628853159..ad0eae3b662d 100644 --- a/packages/android_intent/test/android_intent_test.dart +++ b/packages/android_intent/test/android_intent_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart = 2.9 + import 'package:android_intent/flag.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,8 +14,11 @@ import 'package:platform/platform.dart'; void main() { AndroidIntent androidIntent; MockMethodChannel mockChannel; + setUp(() { mockChannel = MockMethodChannel(); + when(mockChannel.invokeMethod('canResolveActivity', any)) + .thenAnswer((realInvocation) async => true); }); group('AndroidIntent', () { diff --git a/packages/battery/battery/CHANGELOG.md b/packages/battery/battery/CHANGELOG.md index 260f3efeb8ad..ca35c96fb569 100644 --- a/packages/battery/battery/CHANGELOG.md +++ b/packages/battery/battery/CHANGELOG.md @@ -1,3 +1,15 @@ +## 1.0.11 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 1.0.10 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 1.0.9 + +* Update Flutter SDK constraint. + ## 1.0.8 * Update Dart SDK constraint in example. diff --git a/packages/battery/battery/README.md b/packages/battery/battery/README.md index 22ce5007acd7..358dee6f8f27 100644 --- a/packages/battery/battery/README.md +++ b/packages/battery/battery/README.md @@ -1,11 +1,11 @@ # Battery -[![pub package](https://img.shields.io/pub/v/battery.svg)](https://pub.dartlang.org/packages/battery) +[![pub package](https://img.shields.io/pub/v/battery.svg)](https://pub.dev/packages/battery) A Flutter plugin to access various information about the battery of the device the app is running on. ## Usage -To use this plugin, add `battery` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add `battery` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). ### Example diff --git a/packages/battery/battery/example/lib/main.dart b/packages/battery/battery/example/lib/main.dart index 1c1dfcf252b6..c84f5eec519b 100644 --- a/packages/battery/battery/example/lib/main.dart +++ b/packages/battery/battery/example/lib/main.dart @@ -71,7 +71,7 @@ class _MyHomePageState extends State { builder: (_) => AlertDialog( content: Text('Battery: $batteryLevel%'), actions: [ - FlatButton( + TextButton( child: const Text('OK'), onPressed: () { Navigator.pop(context); diff --git a/packages/battery/battery/pubspec.yaml b/packages/battery/battery/pubspec.yaml index ec747190ac69..9c2c2766c85f 100644 --- a/packages/battery/battery/pubspec.yaml +++ b/packages/battery/battery/pubspec.yaml @@ -2,7 +2,7 @@ name: battery description: Flutter plugin for accessing information about the battery state (full, charging, discharging) on Android and iOS. homepage: https://github.com/flutter/plugins/tree/master/packages/battery/battery -version: 1.0.8 +version: 1.0.11 flutter: plugin: @@ -32,4 +32,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/battery/battery_platform_interface/CHANGELOG.md b/packages/battery/battery_platform_interface/CHANGELOG.md index 6fadda91b380..6fc7228a89f9 100644 --- a/packages/battery/battery_platform_interface/CHANGELOG.md +++ b/packages/battery/battery_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.0-nullsafety + +* Migrate to null safety. + +## 1.0.1 + +- Update Flutter SDK constraint. + ## 1.0.0 - Initial open-source release. diff --git a/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart b/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart index 4a3365cc2475..739812dc95e5 100644 --- a/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart +++ b/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart @@ -11,11 +11,11 @@ import '../battery_platform_interface.dart'; class MethodChannelBattery extends BatteryPlatform { /// The method channel used to interact with the native platform. @visibleForTesting - MethodChannel channel = MethodChannel('plugins.flutter.io/battery'); + final MethodChannel channel = MethodChannel('plugins.flutter.io/battery'); /// The event channel used to interact with the native platform. @visibleForTesting - EventChannel eventChannel = EventChannel('plugins.flutter.io/charging'); + final EventChannel eventChannel = EventChannel('plugins.flutter.io/charging'); /// Method channel for getting battery level. Future batteryLevel() async { @@ -23,7 +23,7 @@ class MethodChannelBattery extends BatteryPlatform { } /// Stream variable for storing battery state. - Stream _onBatteryStateChanged; + Stream? _onBatteryStateChanged; /// Event channel for getting battery change state. Stream onBatteryStateChanged() { @@ -32,7 +32,8 @@ class MethodChannelBattery extends BatteryPlatform { .receiveBroadcastStream() .map((dynamic event) => _parseBatteryState(event)); } - return _onBatteryStateChanged; + + return _onBatteryStateChanged!; } } diff --git a/packages/battery/battery_platform_interface/pubspec.yaml b/packages/battery/battery_platform_interface/pubspec.yaml index 6c571debc7b0..c7c4f5e8395e 100644 --- a/packages/battery/battery_platform_interface/pubspec.yaml +++ b/packages/battery/battery_platform_interface/pubspec.yaml @@ -3,20 +3,20 @@ description: A common platform interface for the battery plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/battery # 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: 1.0.0 +version: 2.0.0-nullsafety dependencies: flutter: sdk: flutter - meta: ^1.1.8 - plugin_platform_interface: ^1.0.2 + meta: ^1.3.0-nullsafety + plugin_platform_interface: ^1.1.0-nullsafety.1 dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 + mockito: ^5.0.0-nullsafety.0 + pedantic: ^1.10.0-nullsafety environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.9.1+hotfix.4" diff --git a/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart b/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart index 65323e4044de..2b17cf97e711 100644 --- a/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart +++ b/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart @@ -12,8 +12,8 @@ import 'package:battery_platform_interface/method_channel/method_channel_battery void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group("$MethodChannelBattery", () { - MethodChannelBattery methodChannelBattery; + group('$MethodChannelBattery', () { + late MethodChannelBattery methodChannelBattery; setUp(() async { methodChannelBattery = MethodChannelBattery(); @@ -32,7 +32,7 @@ void main() { .setMockMethodCallHandler((MethodCall methodCall) async { switch (methodCall.method) { case 'listen': - await ServicesBinding.instance.defaultBinaryMessenger + await ServicesBinding.instance!.defaultBinaryMessenger .handlePlatformMessage( methodChannelBattery.eventChannel.name, methodChannelBattery.eventChannel.codec @@ -48,13 +48,13 @@ void main() { }); /// Test for batetry level call. - test("getBatteryLevel", () async { + test('getBatteryLevel', () async { final int result = await methodChannelBattery.batteryLevel(); expect(result, 90); }); /// Test for battery changed state call. - test("onBatteryChanged", () async { + test('onBatteryChanged', () async { final BatteryState result = await methodChannelBattery.onBatteryStateChanged().first; expect(result, BatteryState.full); diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index c9f9dd0f8c58..1a2b03d93a6a 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,101 @@ +## 0.6.6 + +* Adds auto focus support for Android and iOS implementations. + +## 0.6.5 + +* Adds ImageFormat selection for ImageStream and Video(iOS only). + +## 0.6.4+5 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.6.4+4 + +* Set camera auto focus enabled by default. + +## 0.6.4+3 + +* Detect if selected camera supports auto focus and act accordingly on Android. This solves a problem where front facing cameras are not capturing the picture because auto focus is not supported. + +## 0.6.4+2 + +* Set ImageStreamReader listener to null to prevent stale images when streaming images. + +## 0.6.4+1 + +* Added closeCaptureSession() to stopVideoRecording in Camera.java to fix an Android 6 crash. + +## 0.6.4 + +* Adds auto exposure support for Android and iOS implementations. + +## 0.6.3+4 + +* Revert previous dependency update: Changed dependency on camera_platform_interface to >=1.04 <1.1.0. + +## 0.6.3+3 + +* Updated dependency on camera_platform_interface to ^1.2.0. + +## 0.6.3+2 + +* Fixes crash on Android which occurs after video recording has stopped just before taking a picture. + +## 0.6.3+1 + +* Fixes flash & torch modes not working on some Android devices. + +## 0.6.3 + +* Adds torch mode as a flash mode for Android and iOS implementations. + +## 0.6.2+1 + +* Fix the API documentation for the `CameraController.takePicture` method. + +## 0.6.2 + +* Add zoom support for Android and iOS implementations. + +## 0.6.1+1 + +* Added implementation of the `didFinishProcessingPhoto` on iOS which allows saving image metadata (EXIF) on iOS 11 and up. + +## 0.6.1 + +* Add flash support for Android and iOS implementations. + +## 0.6.0+2 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.6.0+1 + +Updated README to inform users that iOS 10.0+ is needed for use + +## 0.6.0 + +As part of implementing federated architecture and making the interface compatible with the web this version contains the following **breaking changes**: + +Method changes in `CameraController`: +- The `takePicture` method no longer accepts the `path` parameter, but instead returns the captured image as an instance of the `XFile` class; +- The `startVideoRecording` method no longer accepts the `filePath`. Instead the recorded video is now returned as a `XFile` instance when the `stopVideoRecording` method completes; +- The `stopVideoRecording` method now returns the captured video when it completes; +- Added the `buildPreview` method which is now used to implement the CameraPreview widget. + +## 0.5.8+19 + +* Update Flutter SDK constraint. + +## 0.5.8+18 + +* Suppress unchecked warning in Android tests which prevented the tests to compile. + +## 0.5.8+17 + +* Added Android 30 support. + ## 0.5.8+16 * Moved package to camera/camera subdir, to allow for federated implementations. diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md index 39d3ed88abea..f7163818aae3 100644 --- a/packages/camera/camera/README.md +++ b/packages/camera/camera/README.md @@ -1,6 +1,6 @@ # Camera Plugin -[![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dartlang.org/packages/camera) +[![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera) A Flutter plugin for iOS and Android allowing access to the device cameras. @@ -15,10 +15,12 @@ A Flutter plugin for iOS and Android allowing access to the device cameras. ## Installation -First, add `camera` as a [dependency in your pubspec.yaml file](https://flutter.io/using-packages/). +First, add `camera` as a [dependency in your pubspec.yaml file](https://flutter.dev/using-packages/). ### iOS +iOS 10.0 of higher is needed to use the camera plugin. If compiling for any version lower than 10.0 make sure to check the iOS version before using the camera plugin. For example, using the [device_info](https://pub.dev/packages/device_info) plugin. + Add two rows to the `ios/Runner/Info.plist`: * one with the key `Privacy - Camera Usage Description` and a usage description. diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle index d19d6df7b447..0b88fd10fb71 100644 --- a/packages/camera/camera/android/build.gradle +++ b/packages/camera/camera/android/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.0' } } @@ -40,16 +40,16 @@ android { sourceCompatibility = '1.8' targetCompatibility = '1.8' } - dependencies { - implementation 'androidx.annotation:annotation:1.0.0' - implementation 'androidx.core:core:1.0.0' - } testOptions { + unitTests.includeAndroidResources = true unitTests.returnDefaultValues = true } } dependencies { + compileOnly 'androidx.annotation:annotation:1.1.0' testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:3.5.13' + testImplementation 'androidx.test:core:1.3.0' + testImplementation 'org.robolectric:robolectric:4.3' } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 2384393c7d2b..3fc702a2a879 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -1,34 +1,58 @@ +// Copyright 2019 The Chromium 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.camera; import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN; import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.graphics.ImageFormat; +import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCaptureSession.CaptureCallback; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.params.StreamConfigurationMap; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.MeteringRectangle; +import android.hardware.camera2.params.OutputConfiguration; +import android.hardware.camera2.params.SessionConfiguration; import android.media.CamcorderProfile; import android.media.Image; import android.media.ImageReader; import android.media.MediaRecorder; import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.util.Range; +import android.util.Rational; import android.util.Size; import android.view.OrientationEventListener; import android.view.Surface; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.PictureCaptureRequest.State; import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FlashMode; +import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.types.ResolutionPreset; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; import java.io.FileOutputStream; @@ -38,9 +62,18 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.concurrent.Executors; + +@FunctionalInterface +interface ErrorCallback { + void onError(String errorCode, String errorMessage); +} public class Camera { + private static final String TAG = "Camera"; + private final SurfaceTextureEntry flutterTexture; private final CameraManager cameraManager; private final OrientationEventListener orientationEventListener; @@ -50,26 +83,36 @@ public class Camera { private final Size captureSize; private final Size previewSize; private final boolean enableAudio; + private final Context applicationContext; + private final CamcorderProfile recordingProfile; + private final DartMessenger dartMessenger; + private final CameraZoom cameraZoom; + private final CameraCharacteristics cameraCharacteristics; private CameraDevice cameraDevice; private CameraCaptureSession cameraCaptureSession; private ImageReader pictureImageReader; private ImageReader imageStreamReader; - private DartMessenger dartMessenger; private CaptureRequest.Builder captureRequestBuilder; private MediaRecorder mediaRecorder; private boolean recordingVideo; - private CamcorderProfile recordingProfile; + private File videoRecordingFile; private int currentOrientation = ORIENTATION_UNKNOWN; + private FlashMode flashMode; + private ExposureMode exposureMode; + private FocusMode focusMode; + private PictureCaptureRequest pictureCaptureRequest; + private CameraRegions cameraRegions; + private int exposureOffset; + private boolean useAutoFocus = true; + private Range fpsRange; - // Mirrors camera.dart - public enum ResolutionPreset { - low, - medium, - high, - veryHigh, - ultraHigh, - max, + private static final HashMap supportedImageFormats; + // Current supported outputs + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", 35); + supportedImageFormats.put("jpeg", 256); } public Camera( @@ -88,6 +131,11 @@ public Camera( this.flutterTexture = flutterTexture; this.dartMessenger = dartMessenger; this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + this.applicationContext = activity.getApplicationContext(); + this.flashMode = FlashMode.auto; + this.exposureMode = ExposureMode.auto; + this.focusMode = FocusMode.auto; + this.exposureOffset = 0; orientationEventListener = new OrientationEventListener(activity.getApplicationContext()) { @Override @@ -101,19 +149,42 @@ public void onOrientationChanged(int i) { }; orientationEventListener.enable(); - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); - StreamConfigurationMap streamConfigurationMap = - characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - //noinspection ConstantConditions - sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - //noinspection ConstantConditions + cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); + initFps(cameraCharacteristics); + sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); isFrontFacing = - characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT; + cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + == CameraMetadata.LENS_FACING_FRONT; ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); recordingProfile = CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); previewSize = computeBestPreviewSize(cameraName, preset); + cameraZoom = + new CameraZoom( + cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), + cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + } + + private void initFps(CameraCharacteristics cameraCharacteristics) { + try { + Range[] ranges = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + Log.i("Camera", "[FPS Range Available] is:" + range); + if (upper >= 10) { + if (fpsRange == null || upper > fpsRange.getUpper()) { + fpsRange = range; + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + Log.i("Camera", "[FPS Range] is:" + fpsRange); } private void prepareMediaRecorder(String outputFilePath) throws IOException { @@ -129,15 +200,20 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { } @SuppressLint("MissingPermission") - public void open(@NonNull final Result result) throws CameraAccessException { + public void open(String imageFormatGroup) throws CameraAccessException { pictureImageReader = ImageReader.newInstance( captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + Integer imageFormat = supportedImageFormats.get(imageFormatGroup); + if (imageFormat == null) { + Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); + imageFormat = ImageFormat.YUV_420_888; + } + // Used to steam image byte data to dart side. imageStreamReader = - ImageReader.newInstance( - previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); + ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); cameraManager.openCamera( cameraName, @@ -146,17 +222,19 @@ public void open(@NonNull final Result result) throws CameraAccessException { public void onOpened(@NonNull CameraDevice device) { cameraDevice = device; try { + cameraRegions = new CameraRegions(getRegionBoundaries()); startPreview(); + dartMessenger.sendCameraInitializedEvent( + previewSize.getWidth(), + previewSize.getHeight(), + exposureMode, + focusMode, + isExposurePointSupported(), + isFocusPointSupported()); } catch (CameraAccessException e) { - result.error("CameraAccess", e.getMessage(), null); + dartMessenger.sendCameraErrorEvent(e.getMessage()); close(); - return; } - Map reply = new HashMap<>(); - reply.put("textureId", flutterTexture.id()); - reply.put("previewWidth", previewSize.getWidth()); - reply.put("previewHeight", previewSize.getHeight()); - result.success(reply); } @Override @@ -168,7 +246,7 @@ public void onClosed(@NonNull CameraDevice camera) { @Override public void onDisconnected(@NonNull CameraDevice cameraDevice) { close(); - dartMessenger.send(DartMessenger.EventType.ERROR, "The camera was disconnected."); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); } @Override @@ -194,12 +272,124 @@ public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { default: errorDescription = "Unknown camera error"; } - dartMessenger.send(DartMessenger.EventType.ERROR, errorDescription); + dartMessenger.sendCameraErrorEvent(errorDescription); } }, null); } + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); + } + + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + // Close any existing capture session. + closeCaptureSession(); + + // Create a new capture builder. + captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); + + // Build Flutter surface to render to + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + captureRequestBuilder.addTarget(flutterSurface); + + List remainingSurfaces = Arrays.asList(surfaces); + if (templateType != CameraDevice.TEMPLATE_PREVIEW) { + // If it is not preview mode, add all surfaces as targets. + for (Surface surface : remainingSurfaces) { + captureRequestBuilder.addTarget(surface); + } + } + + // Prepare the callback + CameraCaptureSession.StateCallback callback = + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; + } + cameraCaptureSession = session; + + updateFpsRange(); + updateFocus(focusMode); + updateFlash(flashMode); + updateExposure(exposureMode); + + refreshPreviewCaptureSession( + onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); + } + }; + + // Start the session + if (VERSION.SDK_INT >= VERSION_CODES.P) { + // Collect all surfaces we want to render to. + List configs = new ArrayList<>(); + configs.add(new OutputConfiguration(flutterSurface)); + for (Surface surface : remainingSurfaces) { + configs.add(new OutputConfiguration(surface)); + } + createCaptureSessionWithSessionConfig(configs, callback); + } else { + // Collect all surfaces we want to render to. + List surfaceList = new ArrayList<>(); + surfaceList.add(flutterSurface); + surfaceList.addAll(remainingSurfaces); + createCaptureSession(surfaceList, callback); + } + } + + @TargetApi(VERSION_CODES.P) + private void createCaptureSessionWithSessionConfig( + List outputConfigs, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession( + new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + callback)); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("deprecation") + private void createCaptureSession( + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession(surfaces, callback, null); + } + + private void refreshPreviewCaptureSession( + @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + if (cameraCaptureSession == null) { + return; + } + + try { + cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), + pictureCaptureCallback, + new Handler(Looper.getMainLooper())); + + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); + } + } + private void writeToFile(ByteBuffer buffer, File file) throws IOException { try (FileOutputStream outputStream = new FileOutputStream(file)) { while (0 < buffer.remaining()) { @@ -208,143 +398,229 @@ private void writeToFile(ByteBuffer buffer, File file) throws IOException { } } - SurfaceTextureEntry getFlutterTexture() { - return flutterTexture; - } - - public void takePicture(String filePath, @NonNull final Result result) { - final File file = new File(filePath); + public void takePicture(@NonNull final Result result) { + // Only take 1 picture at a time + if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); + return; + } + // Store the result + this.pictureCaptureRequest = new PictureCaptureRequest(result); - if (file.exists()) { - result.error( - "fileExists", "File at path '" + filePath + "' already exists. Cannot overwrite.", null); + // Create temporary file + final File outputDir = applicationContext.getCacheDir(); + final File file; + try { + file = File.createTempFile("CAP", ".jpg", outputDir); + } catch (IOException | SecurityException e) { + pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); return; } + // Listen for picture being taken pictureImageReader.setOnImageAvailableListener( reader -> { try (Image image = reader.acquireLatestImage()) { ByteBuffer buffer = image.getPlanes()[0].getBuffer(); writeToFile(buffer, file); - result.success(null); + pictureCaptureRequest.finish(file.getAbsolutePath()); } catch (IOException e) { - result.error("IOError", "Failed saving image", null); + pictureCaptureRequest.error("IOError", "Failed saving image", null); } }, null); + if (useAutoFocus) { + runPictureAutoFocus(); + } else { + runPicturePreCapture(); + } + } + + private final CameraCaptureSession.CaptureCallback pictureCaptureCallback = + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + processCapture(result); + } + + @Override + public void onCaptureProgressed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + processCapture(partialResult); + } + + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + if (pictureCaptureRequest == null || pictureCaptureRequest.isFinished()) { + return; + } + String reason; + switch (failure.getReason()) { + case CaptureFailure.REASON_ERROR: + reason = "An error happened in the framework"; + break; + case CaptureFailure.REASON_FLUSHED: + reason = "The capture has failed due to an abortCaptures() call"; + break; + default: + reason = "Unknown reason"; + } + pictureCaptureRequest.error("captureFailure", reason, null); + } + + private void processCapture(CaptureResult result) { + if (pictureCaptureRequest == null) { + return; + } + + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + switch (pictureCaptureRequest.getState()) { + case focusing: + if (afState == null) { + return; + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // Some devices might return null here, in which case we will also continue. + if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + runPictureCapture(); + } else { + runPicturePreCapture(); + } + } + break; + case preCapture: + // Some devices might return null here, in which case we will also continue. + if (aeState == null + || aeState == CaptureRequest.CONTROL_AE_STATE_PRECAPTURE + || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED + || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { + pictureCaptureRequest.setState(State.waitingPreCaptureReady); + } + break; + case waitingPreCaptureReady: + if (aeState == null || aeState != CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) { + runPictureCapture(); + } + } + } + }; + + private void runPictureAutoFocus() { + assert (pictureCaptureRequest != null); + + pictureCaptureRequest.setState(PictureCaptureRequest.State.focusing); + lockAutoFocus(pictureCaptureCallback); + } + + private void runPicturePreCapture() { + assert (pictureCaptureRequest != null); + pictureCaptureRequest.setState(PictureCaptureRequest.State.preCapture); + + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + + refreshPreviewCaptureSession( + () -> + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE), + (code, message) -> pictureCaptureRequest.error(code, message, null)); + } + + private void runPictureCapture() { + assert (pictureCaptureRequest != null); + pictureCaptureRequest.setState(PictureCaptureRequest.State.capturing); try { final CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); captureBuilder.addTarget(pictureImageReader.getSurface()); captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); - + switch (flashMode) { + case off: + captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case auto: + captureBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + break; + case always: + default: + captureBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + break; + } + cameraCaptureSession.stopRepeating(); cameraCaptureSession.capture( captureBuilder.build(), new CameraCaptureSession.CaptureCallback() { @Override - public void onCaptureFailed( + public void onCaptureCompleted( @NonNull CameraCaptureSession session, @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - String reason; - switch (failure.getReason()) { - case CaptureFailure.REASON_ERROR: - reason = "An error happened in the framework"; - break; - case CaptureFailure.REASON_FLUSHED: - reason = "The capture has failed due to an abortCaptures() call"; - break; - default: - reason = "Unknown reason"; - } - result.error("captureFailure", reason, null); + @NonNull TotalCaptureResult result) { + unlockAutoFocus(); } }, null); } catch (CameraAccessException e) { - result.error("cameraAccess", e.getMessage(), null); + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } } - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { - createCaptureSession(templateType, null, surfaces); - } - - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - // Close any existing capture session. - closeCaptureSession(); + private void lockAutoFocus(CaptureCallback callback) { + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); - // Create a new capture builder. - captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); - - // Build Flutter surface to render to - SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - Surface flutterSurface = new Surface(surfaceTexture); - captureRequestBuilder.addTarget(flutterSurface); + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + } - List remainingSurfaces = Arrays.asList(surfaces); - if (templateType != CameraDevice.TEMPLATE_PREVIEW) { - // If it is not preview mode, add all surfaces as targets. - for (Surface surface : remainingSurfaces) { - captureRequestBuilder.addTarget(surface); - } + private void unlockAutoFocus() { + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + updateFocus(focusMode); + try { + cameraCaptureSession.capture(captureRequestBuilder.build(), null, null); + } catch (CameraAccessException ignored) { } + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); - // Prepare the callback - CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - try { - if (cameraDevice == null) { - dartMessenger.send( - DartMessenger.EventType.ERROR, "The camera was closed during configuration."); - return; - } - cameraCaptureSession = session; - captureRequestBuilder.set( - CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - dartMessenger.send(DartMessenger.EventType.ERROR, e.getMessage()); - } - } - - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - dartMessenger.send( - DartMessenger.EventType.ERROR, "Failed to configure camera session."); - } - }; - - // Collect all surfaces we want to render to. - List surfaceList = new ArrayList<>(); - surfaceList.add(flutterSurface); - surfaceList.addAll(remainingSurfaces); - // Start the session - cameraDevice.createCaptureSession(surfaceList, callback, null); + refreshPreviewCaptureSession( + null, + (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); } - public void startVideoRecording(String filePath, Result result) { - if (new File(filePath).exists()) { - result.error("fileExists", "File at path '" + filePath + "' already exists.", null); + public void startVideoRecording(Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); return; } + try { - prepareMediaRecorder(filePath); + prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); recordingVideo = true; createCaptureSession( CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); result.success(null); } catch (CameraAccessException | IOException e) { + recordingVideo = false; + videoRecordingFile = null; result.error("videoRecordingFailed", e.getMessage(), null); } } @@ -357,10 +633,18 @@ public void stopVideoRecording(@NonNull final Result result) { try { recordingVideo = false; - mediaRecorder.stop(); + + try { + cameraCaptureSession.abortCaptures(); + mediaRecorder.stop(); + } catch (CameraAccessException | IllegalStateException e) { + // Ignore exceptions and try to continue (changes are camera session already aborted capture) + } + mediaRecorder.reset(); startPreview(); - result.success(null); + result.success(videoRecordingFile.getAbsolutePath()); + videoRecordingFile = null; } catch (CameraAccessException | IllegalStateException e) { result.error("videoRecordingFailed", e.getMessage(), null); } @@ -409,6 +693,377 @@ public void resumeVideoRecording(@NonNull final Result result) { result.success(null); } + public void setFlashMode(@NonNull final Result result, FlashMode mode) + throws CameraAccessException { + // Get the flash availability + Boolean flashAvailable = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + + // Check if flash is available. + if (flashAvailable == null || !flashAvailable) { + result.error("setFlashModeFailed", "Device does not have flash capabilities", null); + return; + } + + // If switching directly from torch to auto or on, make sure we turn off the torch. + if (flashMode == FlashMode.torch && mode != FlashMode.torch && mode != FlashMode.off) { + updateFlash(FlashMode.off); + + this.cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), + new CaptureCallback() { + private boolean isFinished = false; + + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult captureResult) { + if (isFinished) { + return; + } + + updateFlash(mode); + refreshPreviewCaptureSession( + () -> { + result.success(null); + isFinished = true; + }, + (code, message) -> + result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } + + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + if (isFinished) { + return; + } + + result.error("setFlashModeFailed", "Could not set flash mode.", null); + isFinished = true; + } + }, + null); + } else { + updateFlash(mode); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } + } + + public void setExposureMode(@NonNull final Result result, ExposureMode mode) + throws CameraAccessException { + updateExposure(mode); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + result.success(null); + } + + public void setExposurePoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if exposure point functionality is available. + if (!isExposurePointSupported()) { + result.error( + "setExposurePointFailed", "Device does not have exposure point capabilities", null); + return; + } + // Check if the current region boundaries are known + if (cameraRegions.getMaxBoundaries() == null) { + result.error("setExposurePointFailed", "Could not determine max region boundaries", null); + return; + } + // Set the metering rectangle + if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); + else cameraRegions.setAutoExposureMeteringRectangleFromPoint(x, y); + // Apply it + updateExposure(exposureMode); + refreshPreviewCaptureSession( + () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); + } + + public void setFocusMode(@NonNull final Result result, FocusMode mode) + throws CameraAccessException { + this.focusMode = mode; + + updateFocus(mode); + + switch (mode) { + case auto: + refreshPreviewCaptureSession( + null, (code, message) -> result.error("setFocusMode", message, null)); + break; + case locked: + lockAutoFocus( + new CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + unlockAutoFocus(); + } + }); + break; + } + result.success(null); + } + + public void setFocusPoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if focus point functionality is available. + if (!isFocusPointSupported()) { + result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); + return; + } + + // Check if the current region boundaries are known + if (cameraRegions.getMaxBoundaries() == null) { + result.error("setFocusPointFailed", "Could not determine max region boundaries", null); + return; + } + + // Set the metering rectangle + if (x == null || y == null) { + cameraRegions.resetAutoFocusMeteringRectangle(); + } else { + cameraRegions.setAutoFocusMeteringRectangleFromPoint(x, y); + } + + // Apply the new metering rectangle + setFocusMode(result, focusMode); + } + + @TargetApi(VERSION_CODES.P) + private boolean supportsDistortionCorrection() throws CameraAccessException { + int[] availableDistortionCorrectionModes = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; + long nonOffModesSupported = + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); + return nonOffModesSupported > 0; + } + + private Size getRegionBoundaries() throws CameraAccessException { + // No distortion correction support + if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { + return cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + } + // Get the current distortion correction mode + Integer distortionCorrectionMode = + captureRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + // Return the correct boundaries depending on the mode + android.graphics.Rect rect; + if (distortionCorrectionMode == null + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + } else { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + } + return rect == null ? null : new Size(rect.width(), rect.height()); + } + + private boolean isExposurePointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + return supportedRegions != null && supportedRegions > 0; + } + + private boolean isFocusPointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + return supportedRegions != null && supportedRegions > 0; + } + + public double getMinExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double minStepped = range == null ? 0 : range.getLower(); + double stepSize = getExposureOffsetStepSize(); + return minStepped * stepSize; + } + + public double getMaxExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double maxStepped = range == null ? 0 : range.getUpper(); + double stepSize = getExposureOffsetStepSize(); + return maxStepped * stepSize; + } + + public double getExposureOffsetStepSize() throws CameraAccessException { + Rational stepSize = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + return stepSize == null ? 0.0 : stepSize.doubleValue(); + } + + public void setExposureOffset(@NonNull final Result result, double offset) + throws CameraAccessException { + // Set the exposure offset + double stepSize = getExposureOffsetStepSize(); + exposureOffset = (int) (offset / stepSize); + // Apply it + updateExposure(exposureMode); + this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + result.success(offset); + } + + public float getMaxZoomLevel() { + return cameraZoom.maxZoom; + } + + public float getMinZoomLevel() { + return CameraZoom.DEFAULT_ZOOM_FACTOR; + } + + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + float maxZoom = cameraZoom.maxZoom; + float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; + + if (zoom > maxZoom || zoom < minZoom) { + String errorMessage = + String.format( + Locale.ENGLISH, + "Zoom level out of bounds (zoom level should be between %f and %f).", + minZoom, + maxZoom); + result.error("ZOOM_ERROR", errorMessage, null); + return; + } + + //Zoom area is calculated relative to sensor area (activeRect) + if (captureRequestBuilder != null) { + final Rect computedZoom = cameraZoom.computeZoom(zoom); + captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + } + + result.success(null); + } + + private void updateFpsRange() { + if (fpsRange == null) { + return; + } + + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); + } + + private void updateFocus(FocusMode mode) { + if (useAutoFocus) { + int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + // Auto focus is not supported + if (modes == null + || modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { + useAutoFocus = false; + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + } else { + // Applying auto focus + switch (mode) { + case locked: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + break; + case auto: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + recordingVideo + ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + default: + break; + } + MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + afRect == null ? null : new MeteringRectangle[] {afRect}); + } + } else { + captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + } + } + + private void updateExposure(ExposureMode mode) { + exposureMode = mode; + + // Applying auto exposure + MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); + + switch (mode) { + case locked: + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + break; + case auto: + default: + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + break; + } + + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); + } + + private void updateFlash(FlashMode mode) { + // Get flash + flashMode = mode; + + // Applying flash modes + switch (flashMode) { + case off: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case auto: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case always: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case torch: + default: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); + break; + } + } + public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; @@ -466,6 +1121,13 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i null); } + public void stopImageStream() throws CameraAccessException { + if (imageStreamReader != null) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + startPreview(); + } + private void closeCaptureSession() { if (cameraCaptureSession != null) { cameraCaptureSession.close(); diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java index b4569d2fec07..3529e69a2b0b 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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.camera; import android.Manifest; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java new file mode 100644 index 000000000000..04412a56631f --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java @@ -0,0 +1,76 @@ +// Copyright 2019 The Chromium 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.camera; + +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; + +public final class CameraRegions { + private MeteringRectangle aeMeteringRectangle; + private MeteringRectangle afMeteringRectangle; + private Size maxBoundaries; + + public CameraRegions(Size maxBoundaries) { + assert (maxBoundaries == null || maxBoundaries.getWidth() > 0); + assert (maxBoundaries == null || maxBoundaries.getHeight() > 0); + this.maxBoundaries = maxBoundaries; + } + + public MeteringRectangle getAEMeteringRectangle() { + return aeMeteringRectangle; + } + + public MeteringRectangle getAFMeteringRectangle() { + return afMeteringRectangle; + } + + public Size getMaxBoundaries() { + return this.maxBoundaries; + } + + public void resetAutoExposureMeteringRectangle() { + this.aeMeteringRectangle = null; + } + + public void setAutoExposureMeteringRectangleFromPoint(double x, double y) { + this.aeMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); + } + + public void resetAutoFocusMeteringRectangle() { + this.afMeteringRectangle = null; + } + + public void setAutoFocusMeteringRectangleFromPoint(double x, double y) { + this.afMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); + } + + public MeteringRectangle getMeteringRectangleForPoint(Size maxBoundaries, double x, double y) { + assert (x >= 0 && x <= 1); + assert (y >= 0 && y <= 1); + if (maxBoundaries == null) + throw new IllegalStateException( + "Functionality for managing metering rectangles is unavailable as this CameraRegions instance was initialized with null boundaries."); + + // Interpolate the target coordinate + int targetX = (int) Math.round(x * ((double) (maxBoundaries.getWidth() - 1))); + int targetY = (int) Math.round(y * ((double) (maxBoundaries.getHeight() - 1))); + // Determine the dimensions of the metering triangle (10th of the viewport) + int targetWidth = (int) Math.round(((double) maxBoundaries.getWidth()) / 10d); + int targetHeight = (int) Math.round(((double) maxBoundaries.getHeight()) / 10d); + // Adjust target coordinate to represent top-left corner of metering rectangle + targetX -= targetWidth / 2; + targetY -= targetHeight / 2; + // Adjust target coordinate as to not fall out of bounds + if (targetX < 0) targetX = 0; + if (targetY < 0) targetY = 0; + int maxTargetX = maxBoundaries.getWidth() - 1 - targetWidth; + int maxTargetY = maxBoundaries.getHeight() - 1 - targetHeight; + if (targetX > maxTargetX) targetX = maxTargetX; + if (targetY > maxTargetY) targetY = maxTargetY; + + // Build the metering rectangle + return new MeteringRectangle(targetX, targetY, targetWidth, targetHeight, 1); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index a7bb3b7d4914..6f04dc80e102 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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.camera; import android.app.Activity; @@ -10,7 +14,7 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.CamcorderProfile; import android.util.Size; -import io.flutter.plugins.camera.Camera.ResolutionPreset; +import io.flutter.plugins.camera.types.ResolutionPreset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java new file mode 100644 index 000000000000..5eed9f4734b7 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java @@ -0,0 +1,52 @@ +// Copyright 2019 The Chromium 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.camera; + +import android.graphics.Rect; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.math.MathUtils; + +public final class CameraZoom { + public static final float DEFAULT_ZOOM_FACTOR = 1.0f; + + @NonNull private final Rect cropRegion = new Rect(); + @Nullable private final Rect sensorSize; + + public final float maxZoom; + public final boolean hasSupport; + + public CameraZoom(@Nullable final Rect sensorArraySize, final Float maxZoom) { + this.sensorSize = sensorArraySize; + + if (this.sensorSize == null) { + this.maxZoom = DEFAULT_ZOOM_FACTOR; + this.hasSupport = false; + return; + } + + this.maxZoom = + ((maxZoom == null) || (maxZoom < DEFAULT_ZOOM_FACTOR)) ? DEFAULT_ZOOM_FACTOR : maxZoom; + + this.hasSupport = (Float.compare(this.maxZoom, DEFAULT_ZOOM_FACTOR) > 0); + } + + public Rect computeZoom(final float zoom) { + if (sensorSize == null || !this.hasSupport) { + return null; + } + + final float newZoom = MathUtils.clamp(zoom, DEFAULT_ZOOM_FACTOR, this.maxZoom); + + final int centerX = this.sensorSize.width() / 2; + final int centerY = this.sensorSize.height() / 2; + final int deltaX = (int) ((0.5f * this.sensorSize.width()) / newZoom); + final int deltaY = (int) ((0.5f * this.sensorSize.height()) / newZoom); + + this.cropRegion.set(centerX - deltaX, centerY - deltaY, centerX + deltaX, centerY + deltaY); + + return cropRegion; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index fe385bef7818..ec68ac0acda3 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -1,51 +1,80 @@ +// Copyright 2019 The Chromium 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.camera; import android.text.TextUtils; import androidx.annotation.Nullable; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FocusMode; import java.util.HashMap; import java.util.Map; class DartMessenger { - @Nullable private EventChannel.EventSink eventSink; + @Nullable private MethodChannel channel; enum EventType { ERROR, CAMERA_CLOSING, + INITIALIZED, + } + + DartMessenger(BinaryMessenger messenger, long cameraId) { + channel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); } - DartMessenger(BinaryMessenger messenger, long eventChannelId) { - new EventChannel(messenger, "flutter.io/cameraPlugin/cameraEvents" + eventChannelId) - .setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object arguments, EventChannel.EventSink sink) { - eventSink = sink; - } - - @Override - public void onCancel(Object arguments) { - eventSink = null; - } - }); + void sendCameraInitializedEvent( + Integer previewWidth, + Integer previewHeight, + ExposureMode exposureMode, + FocusMode focusMode, + Boolean exposurePointSupported, + Boolean focusPointSupported) { + assert (previewWidth != null); + assert (previewHeight != null); + assert (exposureMode != null); + assert (focusMode != null); + assert (exposurePointSupported != null); + assert (focusPointSupported != null); + this.send( + EventType.INITIALIZED, + new HashMap() { + { + put("previewWidth", previewWidth.doubleValue()); + put("previewHeight", previewHeight.doubleValue()); + put("exposureMode", exposureMode.toString()); + put("focusMode", focusMode.toString()); + put("exposurePointSupported", exposurePointSupported); + put("focusPointSupported", focusPointSupported); + } + }); } void sendCameraClosingEvent() { - send(EventType.CAMERA_CLOSING, null); + send(EventType.CAMERA_CLOSING); } - void send(EventType eventType, @Nullable String description) { - if (eventSink == null) { - return; - } + void sendCameraErrorEvent(@Nullable String description) { + this.send( + EventType.ERROR, + new HashMap() { + { + if (!TextUtils.isEmpty(description)) put("description", description); + } + }); + } + + void send(EventType eventType) { + send(eventType, new HashMap<>()); + } - Map event = new HashMap<>(); - event.put("eventType", eventType.toString().toLowerCase()); - // Only errors have a description. - if (eventType == EventType.ERROR && !TextUtils.isEmpty(description)) { - event.put("errorDescription", description); + void send(EventType eventType, Map args) { + if (channel == null) { + return; } - eventSink.success(event); + channel.invokeMethod(eventType.toString().toLowerCase(), args); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 132075555f26..36048dbb5176 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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.camera; import android.app.Activity; @@ -10,7 +14,12 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FlashMode; +import io.flutter.plugins.camera.types.FocusMode; import io.flutter.view.TextureRegistry; +import java.util.HashMap; +import java.util.Map; final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private final Activity activity; @@ -49,11 +58,12 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) handleException(e, result); } break; - case "initialize": + case "create": { if (camera != null) { camera.close(); } + cameraPermissions.requestPermissions( activity, permissionsRegistry, @@ -69,12 +79,28 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) result.error(errCode, errDesc, null); } }); - + break; + } + case "initialize": + { + if (camera != null) { + try { + camera.open(call.argument("imageFormatGroup")); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + } else { + result.error( + "cameraNotFound", + "Camera not found. Please call the 'create' method before calling 'initialize'.", + null); + } break; } case "takePicture": { - camera.takePicture(call.argument("path"), result); + camera.takePicture(result); break; } case "prepareForVideoRecording": @@ -85,7 +111,7 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) } case "startVideoRecording": { - camera.startVideoRecording(call.argument("filePath"), result); + camera.startVideoRecording(result); break; } case "stopVideoRecording": @@ -103,6 +129,119 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) camera.resumeVideoRecording(result); break; } + case "setFlashMode": + { + String modeStr = call.argument("mode"); + FlashMode mode = FlashMode.getValueForString(modeStr); + if (mode == null) { + result.error("setFlashModeFailed", "Unknown flash mode " + modeStr, null); + return; + } + try { + camera.setFlashMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposureMode": + { + String modeStr = call.argument("mode"); + ExposureMode mode = ExposureMode.getValueForString(modeStr); + if (mode == null) { + result.error("setExposureModeFailed", "Unknown exposure mode " + modeStr, null); + return; + } + try { + camera.setExposureMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposurePoint": + { + Boolean reset = call.argument("reset"); + Double x = null; + Double y = null; + if (reset == null || !reset) { + x = call.argument("x"); + y = call.argument("y"); + } + try { + camera.setExposurePoint(result, x, y); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMinExposureOffset": + { + try { + result.success(camera.getMinExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMaxExposureOffset": + { + try { + result.success(camera.getMaxExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getExposureOffsetStepSize": + { + try { + result.success(camera.getExposureOffsetStepSize()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposureOffset": + { + try { + camera.setExposureOffset(result, call.argument("offset")); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setFocusMode": + { + String modeStr = call.argument("mode"); + FocusMode mode = FocusMode.getValueForString(modeStr); + if (mode == null) { + result.error("setFocusModeFailed", "Unknown focus mode " + modeStr, null); + return; + } + try { + camera.setFocusMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setFocusPoint": + { + Boolean reset = call.argument("reset"); + Double x = null; + Double y = null; + if (reset == null || !reset) { + x = call.argument("x"); + y = call.argument("y"); + } + try { + camera.setFocusPoint(result, x, y); + } catch (Exception e) { + handleException(e, result); + } + break; + } case "startImageStream": { try { @@ -116,13 +255,56 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) case "stopImageStream": { try { - camera.startPreview(); + camera.stopImageStream(); result.success(null); } catch (Exception e) { handleException(e, result); } break; } + case "getMaxZoomLevel": + { + assert camera != null; + + try { + float maxZoomLevel = camera.getMaxZoomLevel(); + result.success(maxZoomLevel); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMinZoomLevel": + { + assert camera != null; + + try { + float minZoomLevel = camera.getMinZoomLevel(); + result.success(minZoomLevel); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setZoomLevel": + { + assert camera != null; + + Double zoom = call.argument("zoom"); + + if (zoom == null) { + result.error( + "ZOOM_ERROR", "setZoomLevel is called without specifying a zoom level.", null); + return; + } + + try { + camera.setZoomLevel(result, zoom.floatValue()); + } catch (Exception e) { + handleException(e, result); + } + break; + } case "dispose": { if (camera != null) { @@ -157,7 +339,9 @@ private void instantiateCamera(MethodCall call, Result result) throws CameraAcce resolutionPreset, enableAudio); - camera.open(result); + Map reply = new HashMap<>(); + reply.put("cameraId", flutterSurfaceTexture.id()); + result.success(reply); } // We move catching CameraAccessException out of onMethodCall because it causes a crash diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java new file mode 100644 index 000000000000..189f2f1490dc --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -0,0 +1,55 @@ +// Copyright 2019 The Chromium 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.camera; + +import androidx.annotation.Nullable; +import io.flutter.plugin.common.MethodChannel; + +class PictureCaptureRequest { + + enum State { + idle, + focusing, + preCapture, + waitingPreCaptureReady, + capturing, + finished, + error, + } + + private final MethodChannel.Result result; + private State state; + + public PictureCaptureRequest(MethodChannel.Result result) { + this.result = result; + state = State.idle; + } + + public void setState(State state) { + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + this.state = state; + } + + public State getState() { + return state; + } + + public boolean isFinished() { + return state == State.finished || state == State.error; + } + + public void finish(String absolutePath) { + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + result.success(absolutePath); + state = State.finished; + } + + public void error( + String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + result.error(errorCode, errorMessage, errorDetails); + state = State.error; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java index b2309c83a4a5..4c3fb3add230 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -1,6 +1,7 @@ // Copyright 2019 The Chromium 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.camera.media; import android.media.CamcorderProfile; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java new file mode 100644 index 000000000000..595206fa2216 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java @@ -0,0 +1,29 @@ +// Copyright 2019 The Chromium 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.camera.types; + +// Mirrors exposure_mode.dart +public enum ExposureMode { + auto("auto"), + locked("locked"); + + private final String strValue; + + ExposureMode(String strValue) { + this.strValue = strValue; + } + + public static ExposureMode getValueForString(String modeStr) { + for (ExposureMode value : values()) { + if (value.strValue.equals(modeStr)) return value; + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java new file mode 100644 index 000000000000..c4f0998c418a --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -0,0 +1,31 @@ +// Copyright 2019 The Chromium 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.camera.types; + +// Mirrors flash_mode.dart +public enum FlashMode { + off("off"), + auto("auto"), + always("always"), + torch("torch"); + + private final String strValue; + + FlashMode(String strValue) { + this.strValue = strValue; + } + + public static FlashMode getValueForString(String modeStr) { + for (FlashMode value : values()) { + if (value.strValue.equals(modeStr)) return value; + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java new file mode 100644 index 000000000000..b0dba047f7eb --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java @@ -0,0 +1,29 @@ +// Copyright 2019 The Chromium 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.camera.types; + +// Mirrors focus_mode.dart +public enum FocusMode { + auto("auto"), + locked("locked"); + + private final String strValue; + + FocusMode(String strValue) { + this.strValue = strValue; + } + + public static FocusMode getValueForString(String modeStr) { + for (FocusMode value : values()) { + if (value.strValue.equals(modeStr)) return value; + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java new file mode 100644 index 000000000000..1508dcefb293 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java @@ -0,0 +1,15 @@ +// Copyright 2019 The Chromium 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.camera.types; + +// Mirrors camera.dart +public enum ResolutionPreset { + low, + medium, + high, + veryHigh, + ultraHigh, + max, +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java index b622c313258a..2b19b5dbb0d6 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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.camera; import static junit.framework.TestCase.assertEquals; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java new file mode 100644 index 000000000000..99745e56a857 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionsTest.java @@ -0,0 +1,126 @@ +// Copyright 2019 The Chromium 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.camera; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CameraRegionsTest { + + CameraRegions cameraRegions; + + @Before + public void setUp() { + this.cameraRegions = new CameraRegions(new Size(100, 50)); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_x_upper_bound() { + cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 1.5, 0); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_x_lower_bound() { + cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), -0.5, 0); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_y_upper_bound() { + cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 0, 1.5); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_y_lower_bound() { + cameraRegions.getMeteringRectangleForPoint(new Size(10, 10), 0, -0.5); + } + + @Test(expected = IllegalStateException.class) + public void getMeteringRectangleForPoint_should_throw_for_null_boundaries() { + cameraRegions.getMeteringRectangleForPoint(null, 0, -0); + } + + @Test + public void getMeteringRectangleForPoint_should_return_valid_MeteringRectangle() { + MeteringRectangle r; + // Center + r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 0.5, 0.5); + assertEquals(new MeteringRectangle(45, 23, 10, 5, 1), r); + + // Top left + r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 0.0, 0.0); + assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), r); + + // Bottom right + r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 1.0, 1.0); + assertEquals(new MeteringRectangle(89, 44, 10, 5, 1), r); + + // Top left + r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 0.0, 1.0); + assertEquals(new MeteringRectangle(0, 44, 10, 5, 1), r); + + // Top right + r = cameraRegions.getMeteringRectangleForPoint(cameraRegions.getMaxBoundaries(), 1.0, 0.0); + assertEquals(new MeteringRectangle(89, 0, 10, 5, 1), r); + } + + @Test(expected = AssertionError.class) + public void constructor_should_throw_for_0_width_boundary() { + new CameraRegions(new Size(0, 50)); + } + + @Test(expected = AssertionError.class) + public void constructor_should_throw_for_0_height_boundary() { + new CameraRegions(new Size(100, 0)); + } + + @Test + public void constructor_should_initialize() { + CameraRegions cr = new CameraRegions(new Size(100, 50)); + assertEquals(new Size(100, 50), cr.getMaxBoundaries()); + assertNull(cr.getAEMeteringRectangle()); + assertNull(cr.getAFMeteringRectangle()); + } + + @Test + public void setAutoExposureMeteringRectangleFromPoint_should_set_aeMeteringRectangle_for_point() { + CameraRegions cr = new CameraRegions(new Size(100, 50)); + cr.setAutoExposureMeteringRectangleFromPoint(0, 0); + assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), cr.getAEMeteringRectangle()); + } + + @Test + public void resetAutoExposureMeteringRectangle_should_reset_aeMeteringRectangle() { + CameraRegions cr = new CameraRegions(new Size(100, 50)); + cr.setAutoExposureMeteringRectangleFromPoint(0, 0); + assertNotNull(cr.getAEMeteringRectangle()); + cr.resetAutoExposureMeteringRectangle(); + assertNull(cr.getAEMeteringRectangle()); + } + + @Test + public void setAutoFocusMeteringRectangleFromPoint_should_set_afMeteringRectangle_for_point() { + CameraRegions cr = new CameraRegions(new Size(100, 50)); + cr.setAutoFocusMeteringRectangleFromPoint(0, 0); + assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), cr.getAFMeteringRectangle()); + } + + @Test + public void resetAutoFocusMeteringRectangle_should_reset_afMeteringRectangle() { + CameraRegions cr = new CameraRegions(new Size(100, 50)); + cr.setAutoFocusMeteringRectangleFromPoint(0, 0); + assertNotNull(cr.getAFMeteringRectangle()); + cr.resetAutoFocusMeteringRectangle(); + assertNull(cr.getAFMeteringRectangle()); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java new file mode 100644 index 000000000000..8f05da71b5c5 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java @@ -0,0 +1,125 @@ +// Copyright 2019 The Chromium 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.camera; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.graphics.Rect; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CameraZoomTest { + + @Test + public void ctor_when_parameters_are_valid() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final Float maxZoom = 4.0f; + final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); + + assertNotNull(cameraZoom); + assertTrue(cameraZoom.hasSupport); + assertEquals(4.0f, cameraZoom.maxZoom, 0); + assertEquals(1.0f, CameraZoom.DEFAULT_ZOOM_FACTOR, 0); + } + + @Test + public void ctor_when_sensor_size_is_null() { + final Rect sensorSize = null; + final Float maxZoom = 4.0f; + final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); + + assertNotNull(cameraZoom); + assertFalse(cameraZoom.hasSupport); + assertEquals(cameraZoom.maxZoom, 1.0f, 0); + } + + @Test + public void ctor_when_max_zoom_is_null() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final Float maxZoom = null; + final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); + + assertNotNull(cameraZoom); + assertFalse(cameraZoom.hasSupport); + assertEquals(cameraZoom.maxZoom, 1.0f, 0); + } + + @Test + public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final Float maxZoom = 0.5f; + final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); + + assertNotNull(cameraZoom); + assertFalse(cameraZoom.hasSupport); + assertEquals(cameraZoom.maxZoom, 1.0f, 0); + } + + @Test + public void setZoom_when_no_support_should_not_set_scaler_crop_region() { + final CameraZoom cameraZoom = new CameraZoom(null, null); + final Rect computedZoom = cameraZoom.computeZoom(2f); + + assertNull(computedZoom); + } + + @Test + public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_zero() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f); + final Rect computedZoom = cameraZoom.computeZoom(18f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 0); + assertEquals(computedZoom.top, 0); + assertEquals(computedZoom.right, 0); + assertEquals(computedZoom.bottom, 0); + } + + @Test + public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { + final Rect sensorSize = new Rect(0, 0, 100, 100); + final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f); + final Rect computedZoom = cameraZoom.computeZoom(18f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 48); + assertEquals(computedZoom.top, 48); + assertEquals(computedZoom.right, 52); + assertEquals(computedZoom.bottom, 52); + } + + @Test + public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { + final Rect sensorSize = new Rect(0, 0, 100, 100); + final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f); + final Rect computedZoom = cameraZoom.computeZoom(25f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 45); + assertEquals(computedZoom.top, 45); + assertEquals(computedZoom.right, 55); + assertEquals(computedZoom.bottom, 55); + } + + @Test + public void setZoom_when_zoom_is_smaller_then_min_zoom_clamp_to_min_zoom() { + final Rect sensorSize = new Rect(0, 0, 100, 100); + final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f); + final Rect computedZoom = cameraZoom.computeZoom(0.5f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 0); + assertEquals(computedZoom.top, 0); + assertEquals(computedZoom.right, 100); + assertEquals(computedZoom.bottom, 100); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java index c5ea83aa058a..64425b7b8283 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -1,43 +1,41 @@ +// Copyright 2019 The Chromium 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.camera; import static junit.framework.TestCase.assertNull; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; +import androidx.annotation.NonNull; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.StandardMethodCodec; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FocusMode; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.List; -import java.util.Map; import org.junit.Before; import org.junit.Test; public class DartMessengerTest { /** A {@link BinaryMessenger} implementation that does nothing but save its messages. */ private static class FakeBinaryMessenger implements BinaryMessenger { - private BinaryMessageHandler handler; private final List sentMessages = new ArrayList<>(); @Override - public void send(String channel, ByteBuffer message) { + public void send(@NonNull String channel, ByteBuffer message) { sentMessages.add(message); } @Override - public void send(String channel, ByteBuffer message, BinaryReply callback) { + public void send(@NonNull String channel, ByteBuffer message, BinaryReply callback) { send(channel, message); } @Override - public void setMessageHandler(String channel, BinaryMessageHandler handler) { - this.handler = handler; - } - - BinaryMessageHandler getMessageHandler() { - return handler; - } + public void setMessageHandler(@NonNull String channel, BinaryMessageHandler handler) {} List getMessages() { return new ArrayList<>(sentMessages); @@ -54,55 +52,46 @@ public void setUp() { } @Test - public void setsStreamHandler() { - assertNotNull(fakeBinaryMessenger.getMessageHandler()); - } - - @Test - public void send_handlesNullEventSinks() { - dartMessenger.send(DartMessenger.EventType.ERROR, "error description"); + public void sendCameraErrorEvent_includesErrorDescriptions() { + dartMessenger.sendCameraErrorEvent("error description"); List sentMessages = fakeBinaryMessenger.getMessages(); - assertEquals(0, sentMessages.size()); + assertEquals(1, sentMessages.size()); + MethodCall call = decodeSentMessage(sentMessages.get(0)); + assertEquals("error", call.method); + assertEquals("error description", call.argument("description")); } @Test - public void send_includesErrorDescriptions() { - initializeEventSink(); - - dartMessenger.send(DartMessenger.EventType.ERROR, "error description"); + public void sendCameraInitializedEvent_includesPreviewSize() { + dartMessenger.sendCameraInitializedEvent(0, 0, ExposureMode.auto, FocusMode.auto, true, true); List sentMessages = fakeBinaryMessenger.getMessages(); assertEquals(1, sentMessages.size()); - Map event = decodeSentMessage(sentMessages.get(0)); - assertEquals(DartMessenger.EventType.ERROR.toString().toLowerCase(), event.get("eventType")); - assertEquals("error description", event.get("errorDescription")); + MethodCall call = decodeSentMessage(sentMessages.get(0)); + assertEquals("initialized", call.method); + assertEquals(0, (double) call.argument("previewWidth"), 0); + assertEquals(0, (double) call.argument("previewHeight"), 0); + assertEquals("ExposureMode auto", call.argument("exposureMode"), "auto"); + assertEquals("FocusMode continuous", call.argument("focusMode"), "auto"); + assertEquals("exposurePointSupported", call.argument("exposurePointSupported"), true); + assertEquals("focusPointSupported", call.argument("focusPointSupported"), true); } @Test public void sendCameraClosingEvent() { - initializeEventSink(); - dartMessenger.sendCameraClosingEvent(); List sentMessages = fakeBinaryMessenger.getMessages(); assertEquals(1, sentMessages.size()); - Map event = decodeSentMessage(sentMessages.get(0)); - assertEquals( - DartMessenger.EventType.CAMERA_CLOSING.toString().toLowerCase(), event.get("eventType")); - assertNull(event.get("errorDescription")); + MethodCall call = decodeSentMessage(sentMessages.get(0)); + assertEquals("camera_closing", call.method); + assertNull(call.argument("description")); } - private Map decodeSentMessage(ByteBuffer sentMessage) { + private MethodCall decodeSentMessage(ByteBuffer sentMessage) { sentMessage.position(0); - //noinspection unchecked - return (Map) StandardMethodCodec.INSTANCE.decodeEnvelope(sentMessage); - } - private void initializeEventSink() { - MethodCall call = new MethodCall("listen", null); - ByteBuffer encodedCall = StandardMethodCodec.INSTANCE.encodeMethodCall(call); - encodedCall.position(0); - fakeBinaryMessenger.getMessageHandler().onMessage(encodedCall, reply -> {}); + return StandardMethodCodec.INSTANCE.decodeMethodCall(sentMessage); } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java new file mode 100644 index 000000000000..3ede0b7abe3a --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -0,0 +1,101 @@ +// Copyright 2019 The Chromium 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.camera; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import io.flutter.plugin.common.MethodChannel; +import org.junit.Test; + +public class PictureCaptureRequestTest { + + @Test + public void state_is_idle_by_default() { + PictureCaptureRequest req = new PictureCaptureRequest(null); + assertEquals("Default state is idle", req.getState(), PictureCaptureRequest.State.idle); + } + + @Test + public void setState_sets_state() { + PictureCaptureRequest req = new PictureCaptureRequest(null); + req.setState(PictureCaptureRequest.State.focusing); + assertEquals("State is focusing", req.getState(), PictureCaptureRequest.State.focusing); + req.setState(PictureCaptureRequest.State.preCapture); + assertEquals("State is preCapture", req.getState(), PictureCaptureRequest.State.preCapture); + req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); + assertEquals( + "State is waitingPreCaptureReady", + req.getState(), + PictureCaptureRequest.State.waitingPreCaptureReady); + req.setState(PictureCaptureRequest.State.capturing); + assertEquals( + "State is awaitingPreCapture", req.getState(), PictureCaptureRequest.State.capturing); + } + + @Test + public void finish_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult); + // Act + req.finish("/test/path"); + // Test + verify(mockResult).success("/test/path"); + assertEquals("State is finished", req.getState(), PictureCaptureRequest.State.finished); + } + + @Test + public void isFinished_is_true_When_state_is_finished_or_error() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null); + // Test false states + req.setState(PictureCaptureRequest.State.idle); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequest.State.preCapture); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequest.State.capturing); + assertFalse(req.isFinished()); + // Test true states + req.setState(PictureCaptureRequest.State.finished); + assertTrue(req.isFinished()); + req = new PictureCaptureRequest(null); // Refresh + req.setState(PictureCaptureRequest.State.error); + assertTrue(req.isFinished()); + } + + @Test(expected = IllegalStateException.class) + public void finish_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null); + req.setState(PictureCaptureRequest.State.finished); + // Act + req.finish("/test/path"); + } + + @Test + public void error_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult); + // Act + req.error("ERROR_CODE", "Error Message", null); + // Test + verify(mockResult).error("ERROR_CODE", "Error Message", null); + assertEquals("State is error", req.getState(), PictureCaptureRequest.State.error); + } + + @Test(expected = IllegalStateException.class) + public void error_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null); + req.setState(PictureCaptureRequest.State.finished); + // Act + req.error(null, null, null); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java index 622b49b660a2..823975803994 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -1,3 +1,7 @@ +// Copyright 2019 The Chromium 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.camera.media; import static org.junit.Assert.assertNotNull; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java new file mode 100644 index 000000000000..63810f0b5684 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java @@ -0,0 +1,37 @@ +// Copyright 2019 The Chromium 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.camera.types; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class ExposureModeTest { + + @Test + public void getValueForString_returns_correct_values() { + assertEquals( + "Returns ExposureMode.auto for 'auto'", + ExposureMode.getValueForString("auto"), + ExposureMode.auto); + assertEquals( + "Returns ExposureMode.locked for 'locked'", + ExposureMode.getValueForString("locked"), + ExposureMode.locked); + } + + @Test + public void getValueForString_returns_null_for_nonexistant_value() { + assertEquals( + "Returns null for 'nonexistant'", ExposureMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returns_correct_value() { + assertEquals("Returns 'auto' for ExposureMode.auto", ExposureMode.auto.toString(), "auto"); + assertEquals( + "Returns 'locked' for ExposureMode.locked", ExposureMode.locked.toString(), "locked"); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java new file mode 100644 index 000000000000..1f5f0c6272ed --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java @@ -0,0 +1,42 @@ +// Copyright 2019 The Chromium 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.camera.types; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class FlashModeTest { + + @Test + public void getValueForString_returns_correct_values() { + assertEquals( + "Returns FlashMode.off for 'off'", FlashMode.getValueForString("off"), FlashMode.off); + assertEquals( + "Returns FlashMode.auto for 'auto'", FlashMode.getValueForString("auto"), FlashMode.auto); + assertEquals( + "Returns FlashMode.always for 'always'", + FlashMode.getValueForString("always"), + FlashMode.always); + assertEquals( + "Returns FlashMode.torch for 'torch'", + FlashMode.getValueForString("torch"), + FlashMode.torch); + } + + @Test + public void getValueForString_returns_null_for_nonexistant_value() { + assertEquals( + "Returns null for 'nonexistant'", FlashMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returns_correct_value() { + assertEquals("Returns 'off' for FlashMode.off", FlashMode.off.toString(), "off"); + assertEquals("Returns 'auto' for FlashMode.auto", FlashMode.auto.toString(), "auto"); + assertEquals("Returns 'always' for FlashMode.always", FlashMode.always.toString(), "always"); + assertEquals("Returns 'torch' for FlashMode.torch", FlashMode.torch.toString(), "torch"); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java new file mode 100644 index 000000000000..4aa6fadf776b --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java @@ -0,0 +1,34 @@ +// Copyright 2019 The Chromium 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.camera.types; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class FocusModeTest { + + @Test + public void getValueForString_returns_correct_values() { + assertEquals( + "Returns FocusMode.auto for 'auto'", FocusMode.getValueForString("auto"), FocusMode.auto); + assertEquals( + "Returns FocusMode.locked for 'locked'", + FocusMode.getValueForString("locked"), + FocusMode.locked); + } + + @Test + public void getValueForString_returns_null_for_nonexistant_value() { + assertEquals( + "Returns null for 'nonexistant'", FocusMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returns_correct_value() { + assertEquals("Returns 'auto' for FocusMode.auto", FocusMode.auto.toString(), "auto"); + assertEquals("Returns 'locked' for FocusMode.locked", FocusMode.locked.toString(), "locked"); + } +} diff --git a/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml b/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml index f37f82306024..f216a7251bcf 100644 --- a/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml +++ b/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml @@ -37,4 +37,5 @@ android:required="true"/> + diff --git a/packages/camera/camera/example/android/build.gradle b/packages/camera/camera/example/android/build.gradle index 112aa2a87c27..498448e78692 100644 --- a/packages/camera/camera/example/android/build.gradle +++ b/packages/camera/camera/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.0' } } diff --git a/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties index 019065d1d650..01a286e96a21 100644 --- a/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/packages/camera/camera/example/integration_test/camera_test.dart b/packages/camera/camera/example/integration_test/camera_test.dart index ef4646f5ced9..c2e73e0f1563 100644 --- a/packages/camera/camera/example/integration_test/camera_test.dart +++ b/packages/camera/camera/example/integration_test/camera_test.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui'; +import 'package:camera/camera.dart'; import 'package:flutter/painting.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:camera/camera.dart'; import 'package:path_provider/path_provider.dart'; import 'package:video_player/video_player.dart'; import 'package:integration_test/integration_test.dart'; @@ -55,12 +55,10 @@ void main() { 'Capturing photo at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); // Take Picture - final String filePath = - '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg'; - await controller.takePicture(filePath); + final file = await controller.takePicture(); // Load picture - final File fileImage = File(filePath); + final File fileImage = File(file.path); final Image image = await decodeImageFromList(fileImage.readAsBytesSync()); // Verify image dimensions are as expected @@ -102,14 +100,12 @@ void main() { 'Capturing video at $preset (${expectedSize.width}x${expectedSize.height}) using camera ${controller.description.name}'); // Take Video - final String filePath = - '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.mp4'; - await controller.startVideoRecording(filePath); + await controller.startVideoRecording(); sleep(const Duration(milliseconds: 300)); - await controller.stopVideoRecording(); + final file = await controller.stopVideoRecording(); // Load video metadata - final File videoFile = File(filePath); + final File videoFile = File(file.path); final VideoPlayerController videoController = VideoPlayerController.file(videoFile); await videoController.initialize(); @@ -160,13 +156,10 @@ void main() { await controller.initialize(); await controller.prepareForVideoRecording(); - final String filePath = - '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.mp4'; - int startPause; int timePaused = 0; - await controller.startVideoRecording(filePath); + await controller.startVideoRecording(); final int recordingStart = DateTime.now().millisecondsSinceEpoch; sleep(const Duration(milliseconds: 500)); @@ -186,11 +179,11 @@ void main() { sleep(const Duration(milliseconds: 500)); - await controller.stopVideoRecording(); + final file = await controller.stopVideoRecording(); final int recordingTime = DateTime.now().millisecondsSinceEpoch - recordingStart; - final File videoFile = File(filePath); + final File videoFile = File(file.path); final VideoPlayerController videoController = VideoPlayerController.file( videoFile, ); diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 862ee64fb666..d51240a02c14 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,11 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 75201D617916C49BDEDF852A /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 620DDA07C00B5FF2F937CB5B /* libPods-Runner.a */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; @@ -28,8 +24,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -40,7 +34,6 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 483D985F075B951ADBAD218E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 620DDA07C00B5FF2F937CB5B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -48,7 +41,6 @@ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -63,8 +55,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 75201D617916C49BDEDF852A /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -83,9 +73,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -181,6 +169,7 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = 7624MWN53C; }; }; }; @@ -229,7 +218,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 3E30118C54AB12C3EB9EDF27 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -269,9 +258,12 @@ files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${PODS_ROOT}/../Flutter/Flutter.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -315,7 +307,6 @@ /* Begin XCBuildConfiguration section */ 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -372,7 +363,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -426,6 +416,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -447,6 +438,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 3ec6604ad788..681a45172816 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -9,7 +9,6 @@ import 'dart:io'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:video_player/video_player.dart'; class CameraExampleHome extends StatefulWidget { @@ -36,23 +35,65 @@ void logError(String code, String message) => print('Error: $code\nError Message: $message'); class _CameraExampleHomeState extends State - with WidgetsBindingObserver { + with WidgetsBindingObserver, TickerProviderStateMixin { CameraController controller; - String imagePath; - String videoPath; + XFile imageFile; + XFile videoFile; VideoPlayerController videoController; VoidCallback videoPlayerListener; bool enableAudio = true; + double _minAvailableExposureOffset = 0.0; + double _maxAvailableExposureOffset = 0.0; + double _currentExposureOffset = 0.0; + AnimationController _flashModeControlRowAnimationController; + Animation _flashModeControlRowAnimation; + AnimationController _exposureModeControlRowAnimationController; + Animation _exposureModeControlRowAnimation; + AnimationController _focusModeControlRowAnimationController; + Animation _focusModeControlRowAnimation; + double _minAvailableZoom; + double _maxAvailableZoom; + double _currentScale = 1.0; + double _baseScale = 1.0; + + // Counting pointers (number of user fingers on screen) + int _pointers = 0; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + _flashModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _flashModeControlRowAnimation = CurvedAnimation( + parent: _flashModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _exposureModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _exposureModeControlRowAnimation = CurvedAnimation( + parent: _exposureModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _focusModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _focusModeControlRowAnimation = CurvedAnimation( + parent: _focusModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); + _flashModeControlRowAnimationController.dispose(); + _exposureModeControlRowAnimationController.dispose(); super.dispose(); } @@ -102,7 +143,7 @@ class _CameraExampleHomeState extends State ), ), _captureControlRowWidget(), - _toggleAudioWidget(), + _modeControlRowWidget(), Padding( padding: const EdgeInsets.all(5.0), child: Row( @@ -132,30 +173,37 @@ class _CameraExampleHomeState extends State } else { return AspectRatio( aspectRatio: controller.value.aspectRatio, - child: CameraPreview(controller), + child: Listener( + onPointerDown: (_) => _pointers++, + onPointerUp: (_) => _pointers--, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return GestureDetector( + onScaleStart: _handleScaleStart, + onScaleUpdate: _handleScaleUpdate, + onTapDown: (details) => onViewFinderTap(details, constraints), + child: CameraPreview(controller), + ); + }), + ), ); } } - /// Toggle recording audio - Widget _toggleAudioWidget() { - return Padding( - padding: const EdgeInsets.only(left: 25), - child: Row( - children: [ - const Text('Enable Audio:'), - Switch( - value: enableAudio, - onChanged: (bool value) { - enableAudio = value; - if (controller != null) { - onNewCameraSelected(controller.description); - } - }, - ), - ], - ), - ); + void _handleScaleStart(ScaleStartDetails details) { + _baseScale = _currentScale; + } + + Future _handleScaleUpdate(ScaleUpdateDetails details) async { + // When there are not exactly two fingers on screen don't scale + if (_pointers != 2) { + return; + } + + _currentScale = (_baseScale * details.scale) + .clamp(_minAvailableZoom, _maxAvailableZoom); + + await controller.setZoomLevel(_currentScale); } /// Display the thumbnail of the captured image or video. @@ -166,11 +214,11 @@ class _CameraExampleHomeState extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - videoController == null && imagePath == null + videoController == null && imageFile == null ? Container() : SizedBox( child: (videoController == null) - ? Image.file(File(imagePath)) + ? Image.file(File(imageFile.path)) : Container( child: Center( child: AspectRatio( @@ -192,6 +240,223 @@ class _CameraExampleHomeState extends State ); } + /// Display a bar with buttons to change the flash and exposure modes + Widget _modeControlRowWidget() { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + icon: Icon(Icons.flash_on), + color: Colors.blue, + onPressed: controller != null ? onFlashModeButtonPressed : null, + ), + IconButton( + icon: Icon(Icons.exposure), + color: Colors.blue, + onPressed: + controller != null ? onExposureModeButtonPressed : null, + ), + IconButton( + icon: Icon(Icons.filter_center_focus), + color: Colors.blue, + onPressed: controller != null ? onFocusModeButtonPressed : null, + ), + IconButton( + icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), + color: Colors.blue, + onPressed: controller != null ? onAudioModeButtonPressed : null, + ), + ], + ), + _flashModeControlRowWidget(), + _exposureModeControlRowWidget(), + _focusModeControlRowWidget(), + ], + ); + } + + Widget _flashModeControlRowWidget() { + return SizeTransition( + sizeFactor: _flashModeControlRowAnimation, + child: ClipRect( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + icon: Icon(Icons.flash_off), + color: controller?.value?.flashMode == FlashMode.off + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.off) + : null, + ), + IconButton( + icon: Icon(Icons.flash_auto), + color: controller?.value?.flashMode == FlashMode.auto + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.auto) + : null, + ), + IconButton( + icon: Icon(Icons.flash_on), + color: controller?.value?.flashMode == FlashMode.always + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.always) + : null, + ), + IconButton( + icon: Icon(Icons.highlight), + color: controller?.value?.flashMode == FlashMode.torch + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.torch) + : null, + ), + ], + ), + ), + ); + } + + Widget _exposureModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + primary: controller?.value?.exposureMode == ExposureMode.auto + ? Colors.orange + : Colors.blue, + ); + final ButtonStyle styleLocked = TextButton.styleFrom( + primary: controller?.value?.exposureMode == ExposureMode.locked + ? Colors.orange + : Colors.blue, + ); + + return SizeTransition( + sizeFactor: _exposureModeControlRowAnimation, + child: ClipRect( + child: Container( + color: Colors.grey.shade50, + child: Column( + children: [ + Center( + child: Text("Exposure Mode"), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + TextButton( + child: Text('AUTO'), + style: styleAuto, + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.auto) + : null, + onLongPress: () { + if (controller != null) controller.setExposurePoint(null); + showInSnackBar('Resetting exposure point'); + }, + ), + TextButton( + child: Text('LOCKED'), + style: styleLocked, + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.locked) + : null, + ), + ], + ), + Center( + child: Text("Exposure Offset"), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + Text(_minAvailableExposureOffset.toString()), + Slider( + value: _currentExposureOffset, + min: _minAvailableExposureOffset, + max: _maxAvailableExposureOffset, + label: _currentExposureOffset.toString(), + onChanged: _minAvailableExposureOffset == + _maxAvailableExposureOffset + ? null + : setExposureOffset, + ), + Text(_maxAvailableExposureOffset.toString()), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _focusModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + primary: controller?.value?.focusMode == FocusMode.auto + ? Colors.orange + : Colors.blue, + ); + final ButtonStyle styleLocked = TextButton.styleFrom( + primary: controller?.value?.focusMode == FocusMode.locked + ? Colors.orange + : Colors.blue, + ); + + return SizeTransition( + sizeFactor: _focusModeControlRowAnimation, + child: ClipRect( + child: Container( + color: Colors.grey.shade50, + child: Column( + children: [ + Center( + child: Text("Focus Mode"), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + TextButton( + child: Text('AUTO'), + style: styleAuto, + onPressed: controller != null + ? () => onSetFocusModeButtonPressed(FocusMode.auto) + : null, + onLongPress: () { + if (controller != null) controller.setFocusPoint(null); + showInSnackBar('Resetting focus point'); + }, + ), + TextButton( + child: Text('LOCKED'), + style: styleLocked, + onPressed: controller != null + ? () => onSetFocusModeButtonPressed(FocusMode.locked) + : null, + ), + ], + ), + ], + ), + ), + ), + ); + } + /// Display the control bar with buttons to take pictures and record videos. Widget _captureControlRowWidget() { return Row( @@ -276,6 +541,15 @@ class _CameraExampleHomeState extends State _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(message))); } + void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { + final offset = Offset( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ); + controller.setExposurePoint(offset); + controller.setFocusPoint(offset); + } + void onNewCameraSelected(CameraDescription cameraDescription) async { if (controller != null) { await controller.dispose(); @@ -284,6 +558,7 @@ class _CameraExampleHomeState extends State cameraDescription, ResolutionPreset.medium, enableAudio: enableAudio, + imageFormatGroup: ImageFormatGroup.jpeg, ); // If the controller is updated then update the UI. @@ -296,6 +571,10 @@ class _CameraExampleHomeState extends State try { await controller.initialize(); + _minAvailableExposureOffset = await controller.getMinExposureOffset(); + _maxAvailableExposureOffset = await controller.getMaxExposureOffset(); + _maxAvailableZoom = await controller.getMaxZoomLevel(); + _minAvailableZoom = await controller.getMinZoomLevel(); } on CameraException catch (e) { _showCameraException(e); } @@ -306,29 +585,90 @@ class _CameraExampleHomeState extends State } void onTakePictureButtonPressed() { - takePicture().then((String filePath) { + takePicture().then((XFile file) { if (mounted) { setState(() { - imagePath = filePath; + imageFile = file; videoController?.dispose(); videoController = null; }); - if (filePath != null) showInSnackBar('Picture saved to $filePath'); + if (file != null) showInSnackBar('Picture saved to ${file.path}'); } }); } + void onFlashModeButtonPressed() { + if (_flashModeControlRowAnimationController.value == 1) { + _flashModeControlRowAnimationController.reverse(); + } else { + _flashModeControlRowAnimationController.forward(); + _exposureModeControlRowAnimationController.reverse(); + _focusModeControlRowAnimationController.reverse(); + } + } + + void onExposureModeButtonPressed() { + if (_exposureModeControlRowAnimationController.value == 1) { + _exposureModeControlRowAnimationController.reverse(); + } else { + _exposureModeControlRowAnimationController.forward(); + _flashModeControlRowAnimationController.reverse(); + _focusModeControlRowAnimationController.reverse(); + } + } + + void onFocusModeButtonPressed() { + if (_focusModeControlRowAnimationController.value == 1) { + _focusModeControlRowAnimationController.reverse(); + } else { + _focusModeControlRowAnimationController.forward(); + _flashModeControlRowAnimationController.reverse(); + _exposureModeControlRowAnimationController.reverse(); + } + } + + void onAudioModeButtonPressed() { + enableAudio = !enableAudio; + if (controller != null) { + onNewCameraSelected(controller.description); + } + } + + void onSetFlashModeButtonPressed(FlashMode mode) { + setFlashMode(mode).then((_) { + if (mounted) setState(() {}); + showInSnackBar('Flash mode set to ${mode.toString().split('.').last}'); + }); + } + + void onSetExposureModeButtonPressed(ExposureMode mode) { + setExposureMode(mode).then((_) { + if (mounted) setState(() {}); + showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}'); + }); + } + + void onSetFocusModeButtonPressed(FocusMode mode) { + setFocusMode(mode).then((_) { + if (mounted) setState(() {}); + showInSnackBar('Focus mode set to ${mode.toString().split('.').last}'); + }); + } + void onVideoRecordButtonPressed() { - startVideoRecording().then((String filePath) { + startVideoRecording().then((_) { if (mounted) setState(() {}); - if (filePath != null) showInSnackBar('Saving video to $filePath'); }); } void onStopButtonPressed() { - stopVideoRecording().then((_) { + stopVideoRecording().then((file) { if (mounted) setState(() {}); - showInSnackBar('Video recorded to: $videoPath'); + if (file != null) { + showInSnackBar('Video recorded to ${file.path}'); + videoFile = file; + _startVideoPlayer(); + } }); } @@ -346,45 +686,36 @@ class _CameraExampleHomeState extends State }); } - Future startVideoRecording() async { + Future startVideoRecording() async { if (!controller.value.isInitialized) { showInSnackBar('Error: select a camera first.'); - return null; + return; } - final Directory extDir = await getApplicationDocumentsDirectory(); - final String dirPath = '${extDir.path}/Movies/flutter_test'; - await Directory(dirPath).create(recursive: true); - final String filePath = '$dirPath/${timestamp()}.mp4'; - if (controller.value.isRecordingVideo) { // A recording is already started, do nothing. - return null; + return; } try { - videoPath = filePath; - await controller.startVideoRecording(filePath); + await controller.startVideoRecording(); } on CameraException catch (e) { _showCameraException(e); - return null; + return; } - return filePath; } - Future stopVideoRecording() async { + Future stopVideoRecording() async { if (!controller.value.isRecordingVideo) { return null; } try { - await controller.stopVideoRecording(); + return controller.stopVideoRecording(); } on CameraException catch (e) { _showCameraException(e); return null; } - - await _startVideoPlayer(); } Future pauseVideoRecording() async { @@ -413,9 +744,48 @@ class _CameraExampleHomeState extends State } } + Future setFlashMode(FlashMode mode) async { + try { + await controller.setFlashMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setExposureMode(ExposureMode mode) async { + try { + await controller.setExposureMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setExposureOffset(double offset) async { + setState(() { + _currentExposureOffset = offset; + }); + try { + offset = await controller.setExposureOffset(offset); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setFocusMode(FocusMode mode) async { + try { + await controller.setFocusMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + Future _startVideoPlayer() async { - final VideoPlayerController vcontroller = - VideoPlayerController.file(File(videoPath)); + final VideoPlayerController vController = + VideoPlayerController.file(File(videoFile.path)); videoPlayerListener = () { if (videoController != null && videoController.value.size != null) { // Refreshing the state to update video player with the correct ratio. @@ -423,28 +793,24 @@ class _CameraExampleHomeState extends State videoController.removeListener(videoPlayerListener); } }; - vcontroller.addListener(videoPlayerListener); - await vcontroller.setLooping(true); - await vcontroller.initialize(); + vController.addListener(videoPlayerListener); + await vController.setLooping(true); + await vController.initialize(); await videoController?.dispose(); if (mounted) { setState(() { - imagePath = null; - videoController = vcontroller; + imageFile = null; + videoController = vController; }); } - await vcontroller.play(); + await vController.play(); } - Future takePicture() async { + Future takePicture() async { if (!controller.value.isInitialized) { showInSnackBar('Error: select a camera first.'); return null; } - final Directory extDir = await getApplicationDocumentsDirectory(); - final String dirPath = '${extDir.path}/Pictures/flutter_test'; - await Directory(dirPath).create(recursive: true); - final String filePath = '$dirPath/${timestamp()}.jpg'; if (controller.value.isTakingPicture) { // A capture is already pending, do nothing. @@ -452,12 +818,12 @@ class _CameraExampleHomeState extends State } try { - await controller.takePicture(filePath); + XFile file = await controller.takePicture(); + return file; } on CameraException catch (e) { _showCameraException(e); return null; } - return filePath; } void _showCameraException(CameraException e) { diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml index 0d1f03bef437..5d59ebf75c62 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -22,5 +22,5 @@ flutter: uses-material-design: true environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" + sdk: ">=2.7.0 <3.0.0" flutter: ">=1.9.1+hotfix.4 <2.0.0" diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 525c1286717a..298b906ace7b 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -7,6 +7,7 @@ #import #import #import +#import static FlutterError *getFlutterError(NSError *error) { return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] @@ -51,10 +52,10 @@ @implementation FLTSavePhotoDelegate { self = [super init]; NSAssert(self, @"super init cannot be nil"); _path = path; - _result = result; _motionManager = motionManager; _cameraPosition = cameraPosition; selfReference = self; + _result = result; return self; } @@ -75,13 +76,33 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output UIImage *image = [UIImage imageWithCGImage:[UIImage imageWithData:data].CGImage scale:1.0 orientation:[self getImageRotation]]; + // TODO(sigurdm): Consider writing file asynchronously. bool success = [UIImageJPEGRepresentation(image, 1.0) writeToFile:_path atomically:YES]; if (!success) { _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); return; } - _result(nil); + _result(_path); +} + +- (void)captureOutput:(AVCapturePhotoOutput *)output + didFinishProcessingPhoto:(AVCapturePhoto *)photo + error:(NSError *)error API_AVAILABLE(ios(11.0)) { + selfReference = nil; + if (error) { + _result(getFlutterError(error)); + return; + } + + NSData *photoData = [photo fileDataRepresentation]; + + bool success = [photoData writeToFile:_path atomically:YES]; + if (!success) { + _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); + return; + } + _result(_path); } - (UIImageOrientation)getImageRotation { @@ -113,6 +134,136 @@ - (UIImageOrientation)getImageRotation { } @end +// Mirrors FlashMode in flash_mode.dart +typedef enum { + FlashModeOff, + FlashModeAuto, + FlashModeAlways, + FlashModeTorch, +} FlashMode; + +static FlashMode getFlashModeForString(NSString *mode) { + if ([mode isEqualToString:@"off"]) { + return FlashModeOff; + } else if ([mode isEqualToString:@"auto"]) { + return FlashModeAuto; + } else if ([mode isEqualToString:@"always"]) { + return FlashModeAlways; + } else if ([mode isEqualToString:@"torch"]) { + return FlashModeTorch; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown flash mode %@", mode] + }]; + @throw error; + } +} + +static OSType getVideoFormatFromString(NSString *videoFormatString) { + if ([videoFormatString isEqualToString:@"bgra8888"]) { + return kCVPixelFormatType_32BGRA; + } else if ([videoFormatString isEqualToString:@"yuv420"]) { + return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange; + } else { + NSLog(@"The selected imageFormatGroup is not supported by iOS. Defaulting to brga8888"); + return kCVPixelFormatType_32BGRA; + } +} + +static AVCaptureFlashMode getAVCaptureFlashModeForFlashMode(FlashMode mode) { + switch (mode) { + case FlashModeOff: + return AVCaptureFlashModeOff; + case FlashModeAuto: + return AVCaptureFlashModeAuto; + case FlashModeAlways: + return AVCaptureFlashModeOn; + case FlashModeTorch: + default: + return -1; + } +} + +// Mirrors ExposureMode in camera.dart +typedef enum { + ExposureModeAuto, + ExposureModeLocked, + +} ExposureMode; + +static NSString *getStringForExposureMode(ExposureMode mode) { + switch (mode) { + case ExposureModeAuto: + return @"auto"; + case ExposureModeLocked: + return @"locked"; + } + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown string for exposure mode"] + }]; + @throw error; +} + +static ExposureMode getExposureModeForString(NSString *mode) { + if ([mode isEqualToString:@"auto"]) { + return ExposureModeAuto; + } else if ([mode isEqualToString:@"locked"]) { + return ExposureModeLocked; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown exposure mode %@", mode] + }]; + @throw error; + } +} + +// Mirrors FocusMode in camera.dart +typedef enum { + FocusModeAuto, + FocusModeLocked, +} FocusMode; + +static NSString *getStringForFocusMode(FocusMode mode) { + switch (mode) { + case FocusModeAuto: + return @"auto"; + case FocusModeLocked: + return @"locked"; + } + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown string for focus mode"] + }]; + @throw error; +} + +static FocusMode getFocusModeForString(NSString *mode) { + if ([mode isEqualToString:@"auto"]) { + return FocusModeAuto; + } else if ([mode isEqualToString:@"locked"]) { + return FocusModeLocked; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown focus mode %@", mode] + }]; + @throw error; + } +} + // Mirrors ResolutionPreset in camera.dart typedef enum { veryLow, @@ -152,14 +303,12 @@ static ResolutionPreset getResolutionPresetForString(NSString *preset) { @interface FLTCam : NSObject + AVCaptureAudioDataOutputSampleBufferDelegate> @property(readonly, nonatomic) int64_t textureId; @property(nonatomic, copy) void (^onFrameAvailable)(void); @property BOOL enableAudio; -@property(nonatomic) FlutterEventChannel *eventChannel; @property(nonatomic) FLTImageStreamHandler *imageStreamHandler; -@property(nonatomic) FlutterEventSink eventSink; +@property(nonatomic) FlutterMethodChannel *methodChannel; @property(readonly, nonatomic) AVCaptureSession *captureSession; @property(readonly, nonatomic) AVCaptureDevice *captureDevice; @property(readonly, nonatomic) AVCapturePhotoOutput *capturePhotoOutput API_AVAILABLE(ios(10)); @@ -174,6 +323,7 @@ @interface FLTCam : NSObject _videoWriter.status == AVAssetWriterStatusCompleted) { - result(nil); + result(self->_videoRecordingPath); + self->_videoRecordingPath = nil; } else { - self->_eventSink(@{ - @"event" : @"error", - @"errorDescription" : @"AVAssetWriter could not finish writing!" - }); + result([FlutterError errorWithCode:@"IOError" + message:@"AVAssetWriter could not finish writing!" + details:nil]); } }]; } @@ -620,14 +802,164 @@ - (void)stopVideoRecordingWithResult:(FlutterResult)result { } } -- (void)pauseVideoRecording { +- (void)pauseVideoRecordingWithResult:(FlutterResult)result { _isRecordingPaused = YES; _videoIsDisconnected = YES; _audioIsDisconnected = YES; + result(nil); } -- (void)resumeVideoRecording { +- (void)resumeVideoRecordingWithResult:(FlutterResult)result { _isRecordingPaused = NO; + result(nil); +} + +- (void)setFlashModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { + FlashMode mode; + @try { + mode = getFlashModeForString(modeStr); + } @catch (NSError *e) { + result(getFlutterError(e)); + return; + } + if (mode == FlashModeTorch) { + if (!_captureDevice.hasTorch) { + result([FlutterError errorWithCode:@"setFlashModeFailed" + message:@"Device does not support torch mode" + details:nil]); + return; + } + if (!_captureDevice.isTorchAvailable) { + result([FlutterError errorWithCode:@"setFlashModeFailed" + message:@"Torch mode is currently not available" + details:nil]); + return; + } + if (_captureDevice.torchMode != AVCaptureTorchModeOn) { + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setTorchMode:AVCaptureTorchModeOn]; + [_captureDevice unlockForConfiguration]; + } + } else { + if (!_captureDevice.hasFlash) { + result([FlutterError errorWithCode:@"setFlashModeFailed" + message:@"Device does not have flash capabilities" + details:nil]); + return; + } + AVCaptureFlashMode avFlashMode = getAVCaptureFlashModeForFlashMode(mode); + if (![_capturePhotoOutput.supportedFlashModes + containsObject:[NSNumber numberWithInt:((int)avFlashMode)]]) { + result([FlutterError errorWithCode:@"setFlashModeFailed" + message:@"Device does not support this specific flash mode" + details:nil]); + return; + } + if (_captureDevice.torchMode != AVCaptureTorchModeOff) { + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setTorchMode:AVCaptureTorchModeOff]; + [_captureDevice unlockForConfiguration]; + } + } + _flashMode = mode; + result(nil); +} + +- (void)setExposureModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { + ExposureMode mode; + @try { + mode = getExposureModeForString(modeStr); + } @catch (NSError *e) { + result(getFlutterError(e)); + return; + } + _exposureMode = mode; + [self applyExposureMode]; + result(nil); +} + +- (void)applyExposureMode { + [_captureDevice lockForConfiguration:nil]; + switch (_exposureMode) { + case ExposureModeLocked: + [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; + break; + case ExposureModeAuto: + if ([_captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { + [_captureDevice setExposureMode:AVCaptureExposureModeContinuousAutoExposure]; + } else { + [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; + } + break; + } + [_captureDevice unlockForConfiguration]; +} + +- (void)setFocusModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { + FocusMode mode; + @try { + mode = getFocusModeForString(modeStr); + } @catch (NSError *e) { + result(getFlutterError(e)); + return; + } + _focusMode = mode; + [self applyFocusMode]; + result(nil); +} + +- (void)applyFocusMode { + [_captureDevice lockForConfiguration:nil]; + switch (_focusMode) { + case FocusModeLocked: + [_captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; + break; + case FocusModeAuto: + if ([_captureDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { + [_captureDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus]; + } else { + [_captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; + } + break; + } + [_captureDevice unlockForConfiguration]; +} + +- (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y { + if (!_captureDevice.isExposurePointOfInterestSupported) { + result([FlutterError errorWithCode:@"setExposurePointFailed" + message:@"Device does not have exposure point capabilities" + details:nil]); + return; + } + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setExposurePointOfInterest:CGPointMake(y, 1 - x)]; + [_captureDevice unlockForConfiguration]; + // Retrigger auto exposure + [self applyExposureMode]; + result(nil); +} + +- (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y { + if (!_captureDevice.isFocusPointOfInterestSupported) { + result([FlutterError errorWithCode:@"setFocusPointFailed" + message:@"Device does not have focus point capabilities" + details:nil]); + return; + } + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setFocusPointOfInterest:CGPointMake(y, 1 - x)]; + [_captureDevice unlockForConfiguration]; + // Retrigger auto focus + [self applyFocusMode]; + result(nil); +} + +- (void)setExposureOffsetWithResult:(FlutterResult)result offset:(double)offset { + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setExposureTargetBias:offset completionHandler:nil]; + [_captureDevice unlockForConfiguration]; + result(@(offset)); } - (void)startImageStreamWithMessenger:(NSObject *)messenger { @@ -641,8 +973,8 @@ - (void)startImageStreamWithMessenger:(NSObject *)messen _isStreamingImages = YES; } else { - _eventSink( - @{@"event" : @"error", @"errorDescription" : @"Images from camera are already streaming!"}); + [_methodChannel invokeMethod:errorMethod + arguments:@"Images from camera are already streaming!"]; } } @@ -651,8 +983,61 @@ - (void)stopImageStream { _isStreamingImages = NO; _imageStreamHandler = nil; } else { - _eventSink( - @{@"event" : @"error", @"errorDescription" : @"Images from camera are not streaming!"}); + [_methodChannel invokeMethod:errorMethod arguments:@"Images from camera are not streaming!"]; + } +} + +- (void)getMaxZoomLevelWithResult:(FlutterResult)result { + CGFloat maxZoomFactor = [self getMaxAvailableZoomFactor]; + + result([NSNumber numberWithFloat:maxZoomFactor]); +} + +- (void)getMinZoomLevelWithResult:(FlutterResult)result { + CGFloat minZoomFactor = [self getMinAvailableZoomFactor]; + + result([NSNumber numberWithFloat:minZoomFactor]); +} + +- (void)setZoomLevel:(CGFloat)zoom Result:(FlutterResult)result { + CGFloat maxAvailableZoomFactor = [self getMaxAvailableZoomFactor]; + CGFloat minAvailableZoomFactor = [self getMinAvailableZoomFactor]; + + if (maxAvailableZoomFactor < zoom || minAvailableZoomFactor > zoom) { + NSString *errorMessage = [NSString + stringWithFormat:@"Zoom level out of bounds (zoom level should be between %f and %f).", + minAvailableZoomFactor, maxAvailableZoomFactor]; + FlutterError *error = [FlutterError errorWithCode:@"ZOOM_ERROR" + message:errorMessage + details:nil]; + result(error); + return; + } + + NSError *error = nil; + if (![_captureDevice lockForConfiguration:&error]) { + result(getFlutterError(error)); + return; + } + [_captureDevice rampToVideoZoomFactor:zoom withRate:1]; + [_captureDevice unlockForConfiguration]; + + result(nil); +} + +- (CGFloat)getMinAvailableZoomFactor { + if (@available(iOS 11.0, *)) { + return _captureDevice.minAvailableVideoZoomFactor; + } else { + return 1.0; + } +} + +- (CGFloat)getMaxAvailableZoomFactor { + if (@available(iOS 11.0, *)) { + return _captureDevice.maxAvailableVideoZoomFactor; + } else { + return _captureDevice.activeFormat.videoMaxZoomFactor; } } @@ -672,7 +1057,7 @@ - (BOOL)setupWriterForPath:(NSString *)path { error:&error]; NSParameterAssert(_videoWriter); if (error) { - _eventSink(@{@"event" : @"error", @"errorDescription" : error.description}); + [_methodChannel invokeMethod:errorMethod arguments:error.description]; return NO; } NSDictionary *videoSettings = [NSDictionary @@ -713,6 +1098,12 @@ - (BOOL)setupWriterForPath:(NSString *)path { [_audioOutput setSampleBufferDelegate:self queue:_dispatchQueue]; } + if (_flashMode == FlashModeTorch) { + [self.captureDevice lockForConfiguration:nil]; + [self.captureDevice setTorchMode:AVCaptureTorchModeOn]; + [self.captureDevice unlockForConfiguration]; + } + [_videoWriter addInput:_videoWriterInput]; [_captureVideoOutput setSampleBufferDelegate:self queue:_dispatchQueue]; @@ -726,7 +1117,7 @@ - (void)setUpCaptureSessionForAudio { AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:&error]; if (error) { - _eventSink(@{@"event" : @"error", @"errorDescription" : error.description}); + [_methodChannel invokeMethod:errorMethod arguments:error.description]; } // Setup the audio output. _audioOutput = [[AVCaptureAudioDataOutput alloc] init]; @@ -738,10 +1129,8 @@ - (void)setUpCaptureSessionForAudio { [_captureSession addOutput:_audioOutput]; _isAudioSetup = YES; } else { - _eventSink(@{ - @"event" : @"error", - @"errorDescription" : @"Unable to add Audio input/output to session capture" - }); + [_methodChannel invokeMethod:errorMethod + arguments:@"Unable to add Audio input/output to session capture"]; _isAudioSetup = NO; } } @@ -819,7 +1208,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re } else { result(FlutterMethodNotImplemented); } - } else if ([@"initialize" isEqualToString:call.method]) { + } else if ([@"create" isEqualToString:call.method]) { NSString *cameraName = call.arguments[@"cameraName"]; NSString *resolutionPreset = call.arguments[@"resolutionPreset"]; NSNumber *enableAudio = call.arguments[@"enableAudio"]; @@ -829,6 +1218,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re enableAudio:[enableAudio boolValue] dispatchQueue:_dispatchQueue error:&error]; + if (error) { result(getFlutterError(error)); } else { @@ -837,25 +1227,10 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re } int64_t textureId = [_registry registerTexture:cam]; _camera = cam; - __weak CameraPlugin *weakSelf = self; - cam.onFrameAvailable = ^{ - [weakSelf.registry textureFrameAvailable:textureId]; - }; - FlutterEventChannel *eventChannel = [FlutterEventChannel - eventChannelWithName:[NSString - stringWithFormat:@"flutter.io/cameraPlugin/cameraEvents%lld", - textureId] - binaryMessenger:_messenger]; - [eventChannel setStreamHandler:cam]; - cam.eventChannel = eventChannel; + result(@{ - @"textureId" : @(textureId), - @"previewWidth" : @(cam.previewSize.width), - @"previewHeight" : @(cam.previewSize.height), - @"captureWidth" : @(cam.captureSize.width), - @"captureHeight" : @(cam.captureSize.height), + @"cameraId" : @(textureId), }); - [cam start]; } } else if ([@"startImageStream" isEqualToString:call.method]) { [_camera startImageStreamWithMessenger:_messenger]; @@ -863,23 +1238,43 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re } else if ([@"stopImageStream" isEqualToString:call.method]) { [_camera stopImageStream]; result(nil); - } else if ([@"pauseVideoRecording" isEqualToString:call.method]) { - [_camera pauseVideoRecording]; - result(nil); - } else if ([@"resumeVideoRecording" isEqualToString:call.method]) { - [_camera resumeVideoRecording]; - result(nil); } else { NSDictionary *argsMap = call.arguments; - NSUInteger textureId = ((NSNumber *)argsMap[@"textureId"]).unsignedIntegerValue; - if ([@"takePicture" isEqualToString:call.method]) { + NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue; + if ([@"initialize" isEqualToString:call.method]) { + NSString *videoFormatValue = ((NSString *)argsMap[@"imageFormatGroup"]); + videoFormat = getVideoFormatFromString(videoFormatValue); + + __weak CameraPlugin *weakSelf = self; + _camera.onFrameAvailable = ^{ + [weakSelf.registry textureFrameAvailable:cameraId]; + }; + FlutterMethodChannel *methodChannel = [FlutterMethodChannel + methodChannelWithName:[NSString stringWithFormat:@"flutter.io/cameraPlugin/camera%lu", + (unsigned long)cameraId] + binaryMessenger:_messenger]; + _camera.methodChannel = methodChannel; + [methodChannel + invokeMethod:@"initialized" + arguments:@{ + @"previewWidth" : @(_camera.previewSize.width), + @"previewHeight" : @(_camera.previewSize.height), + @"exposureMode" : getStringForExposureMode([_camera exposureMode]), + @"focusMode" : getStringForFocusMode([_camera focusMode]), + @"exposurePointSupported" : + @([_camera.captureDevice isExposurePointOfInterestSupported]), + @"focusPointSupported" : @([_camera.captureDevice isFocusPointOfInterestSupported]), + }]; + [_camera start]; + result(nil); + } else if ([@"takePicture" isEqualToString:call.method]) { if (@available(iOS 10.0, *)) { - [_camera captureToFile:call.arguments[@"path"] result:result]; + [_camera captureToFile:result]; } else { result(FlutterMethodNotImplemented); } } else if ([@"dispose" isEqualToString:call.method]) { - [_registry unregisterTexture:textureId]; + [_registry unregisterTexture:cameraId]; [_camera close]; _dispatchQueue = nil; result(nil); @@ -887,9 +1282,53 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re [_camera setUpCaptureSessionForAudio]; result(nil); } else if ([@"startVideoRecording" isEqualToString:call.method]) { - [_camera startVideoRecordingAtPath:call.arguments[@"filePath"] result:result]; + [_camera startVideoRecordingWithResult:result]; } else if ([@"stopVideoRecording" isEqualToString:call.method]) { [_camera stopVideoRecordingWithResult:result]; + } else if ([@"pauseVideoRecording" isEqualToString:call.method]) { + [_camera pauseVideoRecordingWithResult:result]; + } else if ([@"resumeVideoRecording" isEqualToString:call.method]) { + [_camera resumeVideoRecordingWithResult:result]; + } else if ([@"getMaxZoomLevel" isEqualToString:call.method]) { + [_camera getMaxZoomLevelWithResult:result]; + } else if ([@"getMinZoomLevel" isEqualToString:call.method]) { + [_camera getMinZoomLevelWithResult:result]; + } else if ([@"setZoomLevel" isEqualToString:call.method]) { + CGFloat zoom = ((NSNumber *)argsMap[@"zoom"]).floatValue; + [_camera setZoomLevel:zoom Result:result]; + } else if ([@"setFlashMode" isEqualToString:call.method]) { + [_camera setFlashModeWithResult:result mode:call.arguments[@"mode"]]; + } else if ([@"setExposureMode" isEqualToString:call.method]) { + [_camera setExposureModeWithResult:result mode:call.arguments[@"mode"]]; + } else if ([@"setExposurePoint" isEqualToString:call.method]) { + BOOL reset = ((NSNumber *)call.arguments[@"reset"]).boolValue; + double x = 0.5; + double y = 0.5; + if (!reset) { + x = ((NSNumber *)call.arguments[@"x"]).doubleValue; + y = ((NSNumber *)call.arguments[@"y"]).doubleValue; + } + [_camera setExposurePointWithResult:result x:x y:y]; + } else if ([@"getMinExposureOffset" isEqualToString:call.method]) { + result(@(_camera.captureDevice.minExposureTargetBias)); + } else if ([@"getMaxExposureOffset" isEqualToString:call.method]) { + result(@(_camera.captureDevice.maxExposureTargetBias)); + } else if ([@"getExposureOffsetStepSize" isEqualToString:call.method]) { + result(@(0.0)); + } else if ([@"setExposureOffset" isEqualToString:call.method]) { + [_camera setExposureOffsetWithResult:result + offset:((NSNumber *)call.arguments[@"offset"]).doubleValue]; + } else if ([@"setFocusMode" isEqualToString:call.method]) { + [_camera setFocusModeWithResult:result mode:call.arguments[@"mode"]]; + } else if ([@"setFocusPoint" isEqualToString:call.method]) { + BOOL reset = ((NSNumber *)call.arguments[@"reset"]).boolValue; + double x = 0.5; + double y = 0.5; + if (!reset) { + x = ((NSNumber *)call.arguments[@"x"]).doubleValue; + y = ((NSNumber *)call.arguments[@"y"]).doubleValue; + } + [_camera setFocusPointWithResult:result x:x y:y]; } else { result(FlutterMethodNotImplemented); } diff --git a/packages/camera/camera/lib/camera.dart b/packages/camera/camera/lib/camera.dart index 3b2cd77c5757..d6e32affdd7a 100644 --- a/packages/camera/camera/lib/camera.dart +++ b/packages/camera/camera/lib/camera.dart @@ -2,643 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -part 'camera_image.dart'; - -final MethodChannel _channel = const MethodChannel('plugins.flutter.io/camera'); - -/// The direction the camera is facing. -enum CameraLensDirection { - /// Front facing camera (a user looking at the screen is seen by the camera). - front, - - /// Back facing camera (a user looking at the screen is not seen by the camera). - back, - - /// External camera which may not be mounted to the device. - external, -} - -/// Affect the quality of video recording and image capture: -/// -/// If a preset is not available on the camera being used a preset of lower quality will be selected automatically. -enum ResolutionPreset { - /// 352x288 on iOS, 240p (320x240) on Android - low, - - /// 480p (640x480 on iOS, 720x480 on Android) - medium, - - /// 720p (1280x720) - high, - - /// 1080p (1920x1080) - veryHigh, - - /// 2160p (3840x2160) - ultraHigh, - - /// The highest resolution available. - max, -} - -/// Signature for a callback receiving the a camera image. -/// -/// This is used by [CameraController.startImageStream]. -// ignore: inference_failure_on_function_return_type -typedef onLatestImageAvailable = Function(CameraImage image); - -/// Returns the resolution preset as a String. -String serializeResolutionPreset(ResolutionPreset resolutionPreset) { - switch (resolutionPreset) { - case ResolutionPreset.max: - return 'max'; - case ResolutionPreset.ultraHigh: - return 'ultraHigh'; - case ResolutionPreset.veryHigh: - return 'veryHigh'; - case ResolutionPreset.high: - return 'high'; - case ResolutionPreset.medium: - return 'medium'; - case ResolutionPreset.low: - return 'low'; - } - throw ArgumentError('Unknown ResolutionPreset value'); -} - -CameraLensDirection _parseCameraLensDirection(String string) { - switch (string) { - case 'front': - return CameraLensDirection.front; - case 'back': - return CameraLensDirection.back; - case 'external': - return CameraLensDirection.external; - } - throw ArgumentError('Unknown CameraLensDirection value'); -} - -/// Completes with a list of available cameras. -/// -/// May throw a [CameraException]. -Future> availableCameras() async { - try { - final List> cameras = await _channel - .invokeListMethod>('availableCameras'); - return cameras.map((Map camera) { - return CameraDescription( - name: camera['name'], - lensDirection: _parseCameraLensDirection(camera['lensFacing']), - sensorOrientation: camera['sensorOrientation'], - ); - }).toList(); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } -} - -/// Properties of a camera device. -class CameraDescription { - /// Creates a new camera description with the given properties. - CameraDescription({this.name, this.lensDirection, this.sensorOrientation}); - - /// The name of the camera device. - final String name; - - /// The direction the camera is facing. - final CameraLensDirection lensDirection; - - /// Clockwise angle through which the output image needs to be rotated to be upright on the device screen in its native orientation. - /// - /// **Range of valid values:** - /// 0, 90, 180, 270 - /// - /// On Android, also defines the direction of rolling shutter readout, which - /// is from top to bottom in the sensor's coordinate system. - final int sensorOrientation; - - @override - bool operator ==(Object o) { - return o is CameraDescription && - o.name == name && - o.lensDirection == lensDirection; - } - - @override - int get hashCode { - return hashValues(name, lensDirection); - } - - @override - String toString() { - return '$runtimeType($name, $lensDirection, $sensorOrientation)'; - } -} - -/// This is thrown when the plugin reports an error. -class CameraException implements Exception { - /// Creates a new camera exception with the given error code and description. - CameraException(this.code, this.description); - - /// Error code. - // TODO(bparrishMines): Document possible error codes. - // https://github.com/flutter/flutter/issues/69298 - String code; - - /// Textual description of the error. - String description; - - @override - String toString() => '$runtimeType($code, $description)'; -} - -/// A widget showing a live camera preview. -class CameraPreview extends StatelessWidget { - /// Creates a preview widget for the given camera controller. - const CameraPreview(this.controller); - - /// The controller for the camera that the preview is shown for. - final CameraController controller; - - @override - Widget build(BuildContext context) { - return controller.value.isInitialized - ? Texture(textureId: controller._textureId) - : Container(); - } -} - -/// The state of a [CameraController]. -class CameraValue { - /// Creates a new camera controller state. - const CameraValue({ - this.isInitialized, - this.errorDescription, - this.previewSize, - this.isRecordingVideo, - this.isTakingPicture, - this.isStreamingImages, - bool isRecordingPaused, - }) : _isRecordingPaused = isRecordingPaused; - - /// Creates a new camera controller state for an uninitialzed controller. - const CameraValue.uninitialized() - : this( - isInitialized: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false, - isRecordingPaused: false, - ); - - /// True after [CameraController.initialize] has completed successfully. - final bool isInitialized; - - /// True when a picture capture request has been sent but as not yet returned. - final bool isTakingPicture; - - /// True when the camera is recording (not the same as previewing). - final bool isRecordingVideo; - - /// True when images from the camera are being streamed. - final bool isStreamingImages; - - final bool _isRecordingPaused; - - /// True when camera [isRecordingVideo] and recording is paused. - bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused; - - /// Description of an error state. - /// - /// This is null while the controller is not in an error state. - /// When [hasError] is true this contains the error description. - final String errorDescription; - - /// The size of the preview in pixels. - /// - /// Is `null` until [isInitialized] is `true`. - final Size previewSize; - - /// Convenience getter for `previewSize.height / previewSize.width`. - /// - /// Can only be called when [initialize] is done. - double get aspectRatio => previewSize.height / previewSize.width; - - /// Whether the controller is in an error state. - /// - /// When true [errorDescription] describes the error. - bool get hasError => errorDescription != null; - - /// Creates a modified copy of the object. - /// - /// Explicitly specified fields get the specified value, all other fields get - /// the same value of the current object. - CameraValue copyWith({ - bool isInitialized, - bool isRecordingVideo, - bool isTakingPicture, - bool isStreamingImages, - String errorDescription, - Size previewSize, - bool isRecordingPaused, - }) { - return CameraValue( - isInitialized: isInitialized ?? this.isInitialized, - errorDescription: errorDescription, - previewSize: previewSize ?? this.previewSize, - isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, - isTakingPicture: isTakingPicture ?? this.isTakingPicture, - isStreamingImages: isStreamingImages ?? this.isStreamingImages, - isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, - ); - } - - @override - String toString() { - return '$runtimeType(' - 'isRecordingVideo: $isRecordingVideo, ' - 'isRecordingVideo: $isRecordingVideo, ' - 'isInitialized: $isInitialized, ' - 'errorDescription: $errorDescription, ' - 'previewSize: $previewSize, ' - 'isStreamingImages: $isStreamingImages)'; - } -} - -/// Controls a device camera. -/// -/// Use [availableCameras] to get a list of available cameras. -/// -/// Before using a [CameraController] a call to [initialize] must complete. -/// -/// To show the camera preview on the screen use a [CameraPreview] widget. -class CameraController extends ValueNotifier { - /// Creates a new camera controller in an uninitialized state. - CameraController( - this.description, - this.resolutionPreset, { - this.enableAudio = true, - }) : super(const CameraValue.uninitialized()); - - /// The properties of the camera device controlled by this controller. - final CameraDescription description; - - /// The resolution this controller is targeting. - /// - /// This resolution preset is not guaranteed to be available on the device, - /// if unavailable a lower resolution will be used. - /// - /// See also: [ResolutionPreset]. - final ResolutionPreset resolutionPreset; - - /// Whether to include audio when recording a video. - final bool enableAudio; - - int _textureId; - bool _isDisposed = false; - StreamSubscription _eventSubscription; - StreamSubscription _imageStreamSubscription; - Completer _creatingCompleter; - - /// Checks whether [CameraController.dispose] has completed successfully. - /// - /// This is a no-op when asserts are disabled. - void debugCheckIsDisposed() { - assert(_isDisposed); - } - - /// Initializes the camera on the device. - /// - /// Throws a [CameraException] if the initialization fails. - Future initialize() async { - if (_isDisposed) { - return Future.value(); - } - try { - _creatingCompleter = Completer(); - final Map reply = - await _channel.invokeMapMethod( - 'initialize', - { - 'cameraName': description.name, - 'resolutionPreset': serializeResolutionPreset(resolutionPreset), - 'enableAudio': enableAudio, - }, - ); - _textureId = reply['textureId']; - value = value.copyWith( - isInitialized: true, - previewSize: Size( - reply['previewWidth'].toDouble(), - reply['previewHeight'].toDouble(), - ), - ); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - _eventSubscription = - EventChannel('flutter.io/cameraPlugin/cameraEvents$_textureId') - .receiveBroadcastStream() - .listen(_listener); - _creatingCompleter.complete(); - return _creatingCompleter.future; - } - - /// Prepare the capture session for video recording. - /// - /// Use of this method is optional, but it may be called for performance - /// reasons on iOS. - /// - /// Preparing audio can cause a minor delay in the CameraPreview view on iOS. - /// If video recording is intended, calling this early eliminates this delay - /// that would otherwise be experienced when video recording is started. - /// This operation is a no-op on Android. - /// - /// Throws a [CameraException] if the prepare fails. - Future prepareForVideoRecording() async { - await _channel.invokeMethod('prepareForVideoRecording'); - } - - /// Listen to events from the native plugins. - /// - /// A "cameraClosing" event is sent when the camera is closed automatically by the system (for example when the app go to background). The plugin will try to reopen the camera automatically but any ongoing recording will end. - void _listener(dynamic event) { - final Map map = event; - if (_isDisposed) { - return; - } - - switch (map['eventType']) { - case 'error': - value = value.copyWith(errorDescription: event['errorDescription']); - break; - case 'cameraClosing': - value = value.copyWith(isRecordingVideo: false); - break; - } - } - - /// Captures an image and saves it to [path]. - /// - /// A path can for example be obtained using - /// [path_provider](https://pub.dartlang.org/packages/path_provider). - /// - /// If a file already exists at the provided path an error will be thrown. - /// The file can be read as this function returns. - /// - /// Throws a [CameraException] if the capture fails. - Future takePicture(String path) async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController.', - 'takePicture was called on uninitialized CameraController', - ); - } - if (value.isTakingPicture) { - throw CameraException( - 'Previous capture has not returned yet.', - 'takePicture was called before the previous capture returned.', - ); - } - try { - value = value.copyWith(isTakingPicture: true); - await _channel.invokeMethod( - 'takePicture', - {'textureId': _textureId, 'path': path}, - ); - value = value.copyWith(isTakingPicture: false); - } on PlatformException catch (e) { - value = value.copyWith(isTakingPicture: false); - throw CameraException(e.code, e.message); - } - } - - /// Start streaming images from platform camera. - /// - /// Settings for capturing images on iOS and Android is set to always use the - /// latest image available from the camera and will drop all other images. - /// - /// When running continuously with [CameraPreview] widget, this function runs - /// best with [ResolutionPreset.low]. Running on [ResolutionPreset.high] can - /// have significant frame rate drops for [CameraPreview] on lower end - /// devices. - /// - /// Throws a [CameraException] if image streaming or video recording has - /// already started. - // TODO(bmparr): Add settings for resolution and fps. - Future startImageStream(onLatestImageAvailable onAvailable) async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController', - 'startImageStream was called on uninitialized CameraController.', - ); - } - if (value.isRecordingVideo) { - throw CameraException( - 'A video recording is already started.', - 'startImageStream was called while a video is being recorded.', - ); - } - if (value.isStreamingImages) { - throw CameraException( - 'A camera has started streaming images.', - 'startImageStream was called while a camera was streaming images.', - ); - } - - try { - await _channel.invokeMethod('startImageStream'); - value = value.copyWith(isStreamingImages: true); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - const EventChannel cameraEventChannel = - EventChannel('plugins.flutter.io/camera/imageStream'); - _imageStreamSubscription = - cameraEventChannel.receiveBroadcastStream().listen( - (dynamic imageData) { - onAvailable(CameraImage._fromPlatformData(imageData)); - }, - ); - } - - /// Stop streaming images from platform camera. - /// - /// Throws a [CameraException] if image streaming was not started or video - /// recording was started. - Future stopImageStream() async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController', - 'stopImageStream was called on uninitialized CameraController.', - ); - } - if (value.isRecordingVideo) { - throw CameraException( - 'A video recording is already started.', - 'stopImageStream was called while a video is being recorded.', - ); - } - if (!value.isStreamingImages) { - throw CameraException( - 'No camera is streaming images', - 'stopImageStream was called when no camera is streaming images.', - ); - } - - try { - value = value.copyWith(isStreamingImages: false); - await _channel.invokeMethod('stopImageStream'); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - - await _imageStreamSubscription.cancel(); - _imageStreamSubscription = null; - } - - /// Start a video recording and save the file to [path]. - /// - /// A path can for example be obtained using - /// [path_provider](https://pub.dartlang.org/packages/path_provider). - /// - /// The file is written on the flight as the video is being recorded. - /// If a file already exists at the provided path an error will be thrown. - /// The file can be read as soon as [stopVideoRecording] returns. - /// - /// Throws a [CameraException] if the capture fails. - Future startVideoRecording(String filePath) async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController', - 'startVideoRecording was called on uninitialized CameraController', - ); - } - if (value.isRecordingVideo) { - throw CameraException( - 'A video recording is already started.', - 'startVideoRecording was called when a recording is already started.', - ); - } - if (value.isStreamingImages) { - throw CameraException( - 'A camera has started streaming images.', - 'startVideoRecording was called while a camera was streaming images.', - ); - } - - try { - await _channel.invokeMethod( - 'startVideoRecording', - {'textureId': _textureId, 'filePath': filePath}, - ); - value = value.copyWith(isRecordingVideo: true, isRecordingPaused: false); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - } - - /// Stop recording. - Future stopVideoRecording() async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController', - 'stopVideoRecording was called on uninitialized CameraController', - ); - } - if (!value.isRecordingVideo) { - throw CameraException( - 'No video is recording', - 'stopVideoRecording was called when no video is recording.', - ); - } - try { - value = value.copyWith(isRecordingVideo: false); - await _channel.invokeMethod( - 'stopVideoRecording', - {'textureId': _textureId}, - ); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - } - - /// Pause video recording. - /// - /// This feature is only available on iOS and Android sdk 24+. - Future pauseVideoRecording() async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController', - 'pauseVideoRecording was called on uninitialized CameraController', - ); - } - if (!value.isRecordingVideo) { - throw CameraException( - 'No video is recording', - 'pauseVideoRecording was called when no video is recording.', - ); - } - try { - value = value.copyWith(isRecordingPaused: true); - await _channel.invokeMethod( - 'pauseVideoRecording', - {'textureId': _textureId}, - ); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - } - - /// Resume video recording after pausing. - /// - /// This feature is only available on iOS and Android sdk 24+. - Future resumeVideoRecording() async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController', - 'resumeVideoRecording was called on uninitialized CameraController', - ); - } - if (!value.isRecordingVideo) { - throw CameraException( - 'No video is recording', - 'resumeVideoRecording was called when no video is recording.', - ); - } - try { - value = value.copyWith(isRecordingPaused: false); - await _channel.invokeMethod( - 'resumeVideoRecording', - {'textureId': _textureId}, - ); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - } - - /// Releases the resources of this camera. - @override - Future dispose() async { - if (_isDisposed) { - return; - } - _isDisposed = true; - super.dispose(); - if (_creatingCompleter != null) { - await _creatingCompleter.future; - await _channel.invokeMethod( - 'dispose', - {'textureId': _textureId}, - ); - await _eventSubscription?.cancel(); - } - } -} +export 'src/camera_controller.dart'; +export 'src/camera_image.dart'; +export 'src/camera_preview.dart'; + +export 'package:camera_platform_interface/camera_platform_interface.dart' + show + CameraDescription, + CameraException, + CameraLensDirection, + FlashMode, + ExposureMode, + FocusMode, + ResolutionPreset, + XFile, + ImageFormatGroup; diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart new file mode 100644 index 000000000000..ae79cc4ad367 --- /dev/null +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -0,0 +1,766 @@ +// Copyright 2018 The Chromium 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 'dart:math'; + +import 'package:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pedantic/pedantic.dart'; + +final MethodChannel _channel = const MethodChannel('plugins.flutter.io/camera'); + +/// Signature for a callback receiving the a camera image. +/// +/// This is used by [CameraController.startImageStream]. +// ignore: inference_failure_on_function_return_type +typedef onLatestImageAvailable = Function(CameraImage image); + +/// Completes with a list of available cameras. +/// +/// May throw a [CameraException]. +Future> availableCameras() async { + return CameraPlatform.instance.availableCameras(); +} + +/// The state of a [CameraController]. +class CameraValue { + /// Creates a new camera controller state. + const CameraValue({ + this.isInitialized, + this.errorDescription, + this.previewSize, + this.isRecordingVideo, + this.isTakingPicture, + this.isStreamingImages, + bool isRecordingPaused, + this.flashMode, + this.exposureMode, + this.focusMode, + this.exposurePointSupported, + this.focusPointSupported, + }) : _isRecordingPaused = isRecordingPaused; + + /// Creates a new camera controller state for an uninitialized controller. + const CameraValue.uninitialized() + : this( + isInitialized: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + isRecordingPaused: false, + flashMode: FlashMode.auto, + exposurePointSupported: false, + focusPointSupported: false, + ); + + /// True after [CameraController.initialize] has completed successfully. + final bool isInitialized; + + /// True when a picture capture request has been sent but as not yet returned. + final bool isTakingPicture; + + /// True when the camera is recording (not the same as previewing). + final bool isRecordingVideo; + + /// True when images from the camera are being streamed. + final bool isStreamingImages; + + final bool _isRecordingPaused; + + /// True when camera [isRecordingVideo] and recording is paused. + bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused; + + /// Description of an error state. + /// + /// This is null while the controller is not in an error state. + /// When [hasError] is true this contains the error description. + final String errorDescription; + + /// The size of the preview in pixels. + /// + /// Is `null` until [isInitialized] is `true`. + final Size previewSize; + + /// Convenience getter for `previewSize.height / previewSize.width`. + /// + /// Can only be called when [initialize] is done. + double get aspectRatio => previewSize.height / previewSize.width; + + /// Whether the controller is in an error state. + /// + /// When true [errorDescription] describes the error. + bool get hasError => errorDescription != null; + + /// The flash mode the camera is currently set to. + final FlashMode flashMode; + + /// The exposure mode the camera is currently set to. + final ExposureMode exposureMode; + + /// The focus mode the camera is currently set to. + final FocusMode focusMode; + + /// Whether setting the exposure point is supported. + final bool exposurePointSupported; + + /// Whether setting the focus point is supported. + final bool focusPointSupported; + + /// Creates a modified copy of the object. + /// + /// Explicitly specified fields get the specified value, all other fields get + /// the same value of the current object. + CameraValue copyWith({ + bool isInitialized, + bool isRecordingVideo, + bool isTakingPicture, + bool isStreamingImages, + String errorDescription, + Size previewSize, + bool isRecordingPaused, + FlashMode flashMode, + ExposureMode exposureMode, + FocusMode focusMode, + bool exposurePointSupported, + bool focusPointSupported, + }) { + return CameraValue( + isInitialized: isInitialized ?? this.isInitialized, + errorDescription: errorDescription, + previewSize: previewSize ?? this.previewSize, + isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, + isTakingPicture: isTakingPicture ?? this.isTakingPicture, + isStreamingImages: isStreamingImages ?? this.isStreamingImages, + isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, + flashMode: flashMode ?? this.flashMode, + exposureMode: exposureMode ?? this.exposureMode, + focusMode: focusMode ?? this.focusMode, + exposurePointSupported: + exposurePointSupported ?? this.exposurePointSupported, + focusPointSupported: focusPointSupported ?? this.focusPointSupported, + ); + } + + @override + String toString() { + return '$runtimeType(' + 'isRecordingVideo: $isRecordingVideo, ' + 'isInitialized: $isInitialized, ' + 'errorDescription: $errorDescription, ' + 'previewSize: $previewSize, ' + 'isStreamingImages: $isStreamingImages, ' + 'flashMode: $flashMode, ' + 'exposureMode: $exposureMode, ' + 'focusMode: $focusMode, ' + 'exposurePointSupported: $exposurePointSupported, ' + 'focusPointSupported: $focusPointSupported)'; + } +} + +/// Controls a device camera. +/// +/// Use [availableCameras] to get a list of available cameras. +/// +/// Before using a [CameraController] a call to [initialize] must complete. +/// +/// To show the camera preview on the screen use a [CameraPreview] widget. +class CameraController extends ValueNotifier { + /// Creates a new camera controller in an uninitialized state. + CameraController( + this.description, + this.resolutionPreset, { + this.enableAudio = true, + this.imageFormatGroup, + }) : super(const CameraValue.uninitialized()); + + /// The properties of the camera device controlled by this controller. + final CameraDescription description; + + /// The resolution this controller is targeting. + /// + /// This resolution preset is not guaranteed to be available on the device, + /// if unavailable a lower resolution will be used. + /// + /// See also: [ResolutionPreset]. + final ResolutionPreset resolutionPreset; + + /// Whether to include audio when recording a video. + final bool enableAudio; + + /// The [ImageFormatGroup] describes the output of the raw image format. + /// + /// When null the imageFormat will fallback to the platforms default. + final ImageFormatGroup imageFormatGroup; + + int _cameraId; + bool _isDisposed = false; + StreamSubscription _imageStreamSubscription; + FutureOr _initCalled; + + /// Checks whether [CameraController.dispose] has completed successfully. + /// + /// This is a no-op when asserts are disabled. + void debugCheckIsDisposed() { + assert(_isDisposed); + } + + /// The camera identifier with which the controller is associated. + int get cameraId => _cameraId; + + /// Initializes the camera on the device. + /// + /// Throws a [CameraException] if the initialization fails. + Future initialize() async { + if (_isDisposed) { + throw CameraException( + 'Disposed CameraController', + 'initialize was called on a disposed CameraController', + ); + } + try { + Completer _initializeCompleter = Completer(); + + _cameraId = await CameraPlatform.instance.createCamera( + description, + resolutionPreset, + enableAudio: enableAudio, + ); + + unawaited(CameraPlatform.instance + .onCameraInitialized(_cameraId) + .first + .then((event) { + _initializeCompleter.complete(event); + })); + + await CameraPlatform.instance.initializeCamera( + _cameraId, + imageFormatGroup: imageFormatGroup, + ); + + value = value.copyWith( + isInitialized: true, + previewSize: await _initializeCompleter.future + .then((CameraInitializedEvent event) => Size( + event.previewWidth, + event.previewHeight, + )), + exposureMode: await _initializeCompleter.future + .then((event) => event.exposureMode), + focusMode: + await _initializeCompleter.future.then((event) => event.focusMode), + exposurePointSupported: await _initializeCompleter.future + .then((event) => event.exposurePointSupported), + focusPointSupported: await _initializeCompleter.future + .then((event) => event.focusPointSupported), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + + _initCalled = true; + } + + /// Prepare the capture session for video recording. + /// + /// Use of this method is optional, but it may be called for performance + /// reasons on iOS. + /// + /// Preparing audio can cause a minor delay in the CameraPreview view on iOS. + /// If video recording is intended, calling this early eliminates this delay + /// that would otherwise be experienced when video recording is started. + /// This operation is a no-op on Android. + /// + /// Throws a [CameraException] if the prepare fails. + Future prepareForVideoRecording() async { + await CameraPlatform.instance.prepareForVideoRecording(); + } + + /// Captures an image and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture fails. + Future takePicture() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController.', + 'takePicture was called on uninitialized CameraController', + ); + } + if (value.isTakingPicture) { + throw CameraException( + 'Previous capture has not returned yet.', + 'takePicture was called before the previous capture returned.', + ); + } + try { + value = value.copyWith(isTakingPicture: true); + XFile file = await CameraPlatform.instance.takePicture(_cameraId); + value = value.copyWith(isTakingPicture: false); + return file; + } on PlatformException catch (e) { + value = value.copyWith(isTakingPicture: false); + throw CameraException(e.code, e.message); + } + } + + /// Start streaming images from platform camera. + /// + /// Settings for capturing images on iOS and Android is set to always use the + /// latest image available from the camera and will drop all other images. + /// + /// When running continuously with [CameraPreview] widget, this function runs + /// best with [ResolutionPreset.low]. Running on [ResolutionPreset.high] can + /// have significant frame rate drops for [CameraPreview] on lower end + /// devices. + /// + /// Throws a [CameraException] if image streaming or video recording has + /// already started. + /// + /// The `startImageStream` method is only available on Android and iOS (other + /// platforms won't be supported in current setup). + /// + // TODO(bmparr): Add settings for resolution and fps. + Future startImageStream(onLatestImageAvailable onAvailable) async { + assert(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS); + + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'startImageStream was called on uninitialized CameraController.', + ); + } + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'startImageStream was called while a video is being recorded.', + ); + } + if (value.isStreamingImages) { + throw CameraException( + 'A camera has started streaming images.', + 'startImageStream was called while a camera was streaming images.', + ); + } + + try { + await _channel.invokeMethod('startImageStream'); + value = value.copyWith(isStreamingImages: true); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + const EventChannel cameraEventChannel = + EventChannel('plugins.flutter.io/camera/imageStream'); + _imageStreamSubscription = + cameraEventChannel.receiveBroadcastStream().listen( + (dynamic imageData) { + onAvailable(CameraImage.fromPlatformData(imageData)); + }, + ); + } + + /// Stop streaming images from platform camera. + /// + /// Throws a [CameraException] if image streaming was not started or video + /// recording was started. + /// + /// The `stopImageStream` method is only available on Android and iOS (other + /// platforms won't be supported in current setup). + Future stopImageStream() async { + assert(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS); + + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'stopImageStream was called on uninitialized CameraController.', + ); + } + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'stopImageStream was called while a video is being recorded.', + ); + } + if (!value.isStreamingImages) { + throw CameraException( + 'No camera is streaming images', + 'stopImageStream was called when no camera is streaming images.', + ); + } + + try { + value = value.copyWith(isStreamingImages: false); + await _channel.invokeMethod('stopImageStream'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + + await _imageStreamSubscription.cancel(); + _imageStreamSubscription = null; + } + + /// Start a video recording. + /// + /// The video is returned as a [XFile] after calling [stopVideoRecording]. + /// Throws a [CameraException] if the capture fails. + Future startVideoRecording() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'startVideoRecording was called on uninitialized CameraController', + ); + } + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'startVideoRecording was called when a recording is already started.', + ); + } + if (value.isStreamingImages) { + throw CameraException( + 'A camera has started streaming images.', + 'startVideoRecording was called while a camera was streaming images.', + ); + } + + try { + await CameraPlatform.instance.startVideoRecording(_cameraId); + value = value.copyWith(isRecordingVideo: true, isRecordingPaused: false); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Stops the video recording and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture failed. + Future stopVideoRecording() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'stopVideoRecording was called on uninitialized CameraController', + ); + } + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'stopVideoRecording was called when no video is recording.', + ); + } + try { + XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); + value = value.copyWith(isRecordingVideo: false); + return file; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Pause video recording. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future pauseVideoRecording() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'pauseVideoRecording was called on uninitialized CameraController', + ); + } + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'pauseVideoRecording was called when no video is recording.', + ); + } + try { + await CameraPlatform.instance.pauseVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: true); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resume video recording after pausing. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future resumeVideoRecording() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'resumeVideoRecording was called on uninitialized CameraController', + ); + } + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'resumeVideoRecording was called when no video is recording.', + ); + } + try { + await CameraPlatform.instance.resumeVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: false); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Returns a widget showing a live camera preview. + Widget buildPreview() { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'buildView() was called on uninitialized CameraController.', + ); + } + try { + return CameraPlatform.instance.buildPreview(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the maximum supported zoom level for the selected camera. + Future getMaxZoomLevel() { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'getMaxZoomLevel was called on uninitialized CameraController', + ); + } + + try { + return CameraPlatform.instance.getMaxZoomLevel(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the minimum supported zoom level for the selected camera. + Future getMinZoomLevel() { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'getMinZoomLevel was called on uninitialized CameraController', + ); + } + + try { + return CameraPlatform.instance.getMinZoomLevel(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Set the zoom level for the selected camera. + /// + /// The supplied [zoom] value should be between 1.0 and the maximum supported + /// zoom level returned by the `getMaxZoomLevel`. Throws an `CameraException` + /// when an illegal zoom level is suplied. + Future setZoomLevel(double zoom) { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'setZoomLevel was called on uninitialized CameraController', + ); + } + + try { + return CameraPlatform.instance.setZoomLevel(_cameraId, zoom); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the flash mode for taking pictures. + Future setFlashMode(FlashMode mode) async { + try { + await CameraPlatform.instance.setFlashMode(_cameraId, mode); + value = value.copyWith(flashMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure mode for taking pictures. + Future setExposureMode(ExposureMode mode) async { + try { + await CameraPlatform.instance.setExposureMode(_cameraId, mode); + value = value.copyWith(exposureMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure point for automatically determining the exposure value. + Future setExposurePoint(Offset point) async { + if (point != null && + (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { + throw ArgumentError( + 'The values of point should be anywhere between (0,0) and (1,1).'); + } + try { + await CameraPlatform.instance.setExposurePoint( + _cameraId, + point == null + ? null + : Point( + point.dx, + point.dy, + ), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the minimum supported exposure offset for the selected camera in EV units. + Future getMinExposureOffset() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'getMinExposureOffset was called on uninitialized CameraController', + ); + } + + try { + return CameraPlatform.instance.getMinExposureOffset(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the maximum supported exposure offset for the selected camera in EV units. + Future getMaxExposureOffset() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'getMaxExposureOffset was called on uninitialized CameraController', + ); + } + + try { + return CameraPlatform.instance.getMaxExposureOffset(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the supported step size for exposure offset for the selected camera in EV units. + /// + /// Returns 0 when the camera supports using a free value without stepping. + Future getExposureOffsetStepSize() async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'getExposureOffsetStepSize was called on uninitialized CameraController', + ); + } + + try { + return CameraPlatform.instance.getExposureOffsetStepSize(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure offset for the selected camera. + /// + /// The supplied [offset] value should be in EV units. 1 EV unit represents a + /// doubling in brightness. It should be between the minimum and maximum offsets + /// obtained through `getMinExposureOffset` and `getMaxExposureOffset` respectively. + /// Throws a `CameraException` when an illegal offset is supplied. + /// + /// When the supplied [offset] value does not align with the step size obtained + /// through `getExposureStepSize`, it will automatically be rounded to the nearest step. + /// + /// Returns the (rounded) offset value that was set. + Future setExposureOffset(double offset) async { + if (!value.isInitialized || _isDisposed) { + throw CameraException( + 'Uninitialized CameraController', + 'setExposureOffset was called on uninitialized CameraController', + ); + } + + // Check if offset is in range + List range = + await Future.wait([getMinExposureOffset(), getMaxExposureOffset()]); + if (offset < range[0] || offset > range[1]) { + throw CameraException( + "exposureOffsetOutOfBounds", + "The provided exposure offset was outside the supported range for this device.", + ); + } + + // Round to the closest step if needed + double stepSize = await getExposureOffsetStepSize(); + if (stepSize > 0) { + double inv = 1.0 / stepSize; + double roundedOffset = (offset * inv).roundToDouble() / inv; + if (roundedOffset > range[1]) { + roundedOffset = (offset * inv).floorToDouble() / inv; + } else if (roundedOffset < range[0]) { + roundedOffset = (offset * inv).ceilToDouble() / inv; + } + offset = roundedOffset; + } + + try { + return CameraPlatform.instance.setExposureOffset(_cameraId, offset); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the focus mode for taking pictures. + Future setFocusMode(FocusMode mode) async { + try { + await CameraPlatform.instance.setFocusMode(_cameraId, mode); + value = value.copyWith(focusMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the focus point for automatically determining the focus value. + Future setFocusPoint(Offset point) async { + if (point != null && + (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { + throw ArgumentError( + 'The values of point should be anywhere between (0,0) and (1,1).'); + } + try { + await CameraPlatform.instance.setFocusPoint( + _cameraId, + point == null + ? null + : Point( + point.dx, + point.dy, + ), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Releases the resources of this camera. + @override + Future dispose() async { + if (_isDisposed) { + return; + } + _isDisposed = true; + super.dispose(); + if (_initCalled != null) { + await _initCalled; + await CameraPlatform.instance.dispose(_cameraId); + } + } +} diff --git a/packages/camera/camera/lib/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart similarity index 73% rename from packages/camera/camera/lib/camera_image.dart rename to packages/camera/camera/lib/src/camera_image.dart index cebc14873f52..dffa5066d14f 100644 --- a/packages/camera/camera/lib/camera_image.dart +++ b/packages/camera/camera/lib/src/camera_image.dart @@ -2,7 +2,11 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -part of 'camera.dart'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; /// A single color plane of image data. /// @@ -38,32 +42,6 @@ class Plane { final int width; } -// TODO:(bmparr) Turn [ImageFormatGroup] to a class with int values. -/// Group of image formats that are comparable across Android and iOS platforms. -enum ImageFormatGroup { - /// The image format does not fit into any specific group. - unknown, - - /// Multi-plane YUV 420 format. - /// - /// This format is a generic YCbCr format, capable of describing any 4:2:0 - /// chroma-subsampled planar or semiplanar buffer (but not fully interleaved), - /// with 8 bits per color sample. - /// - /// On Android, this is `android.graphics.ImageFormat.YUV_420_888`. See - /// https://developer.android.com/reference/android/graphics/ImageFormat.html#YUV_420_888 - /// - /// On iOS, this is `kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`. See - /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_420ypcbcr8biplanarvideorange?language=objc - yuv420, - - /// 32-bit BGRA. - /// - /// On iOS, this is `kCVPixelFormatType_32BGRA`. See - /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_32bgra?language=objc - bgra8888, -} - /// Describes how pixels are represented in an image. class ImageFormat { ImageFormat._fromPlatformData(this.raw) : group = _asImageFormatGroup(raw); @@ -83,9 +61,13 @@ class ImageFormat { ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { if (defaultTargetPlatform == TargetPlatform.android) { - // android.graphics.ImageFormat.YUV_420_888 - if (rawFormat == 35) { - return ImageFormatGroup.yuv420; + switch (rawFormat) { + // android.graphics.ImageFormat.YUV_420_888 + case 35: + return ImageFormatGroup.yuv420; + // android.graphics.ImageFormat.JPEG + case 256: + return ImageFormatGroup.jpeg; } } @@ -113,7 +95,8 @@ ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { /// Although not all image formats are planar on iOS, we treat 1-dimensional /// images as single planar images. class CameraImage { - CameraImage._fromPlatformData(Map data) + /// CameraImage Constructor + CameraImage.fromPlatformData(Map data) : format = ImageFormat._fromPlatformData(data['format']), height = data['height'], width = data['width'], diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart new file mode 100644 index 000000000000..bf7862eb9151 --- /dev/null +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -0,0 +1,23 @@ +// Copyright 2018 The Chromium 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:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/material.dart'; + +/// A widget showing a live camera preview. +class CameraPreview extends StatelessWidget { + /// Creates a preview widget for the given camera controller. + const CameraPreview(this.controller); + + /// The controller for the camera that the preview is shown for. + final CameraController controller; + + @override + Widget build(BuildContext context) { + return controller.value.isInitialized + ? CameraPlatform.instance.buildPreview(controller.cameraId) + : Container(); + } +} diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 7088faf227aa..0b21497b5462 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -2,12 +2,14 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. -version: 0.5.8+16 +version: 0.6.6 homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: flutter: sdk: flutter + camera_platform_interface: ^1.5.0 + pedantic: ^1.8.0 dev_dependencies: path_provider: ^0.5.0 @@ -16,7 +18,8 @@ dev_dependencies: sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.8.0 + mockito: ^4.1.3 + plugin_platform_interface: ^1.0.3 flutter: plugin: @@ -28,5 +31,5 @@ flutter: pluginClass: CameraPlugin environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/camera/camera/test/camera_image_stream_test.dart b/packages/camera/camera/test/camera_image_stream_test.dart new file mode 100644 index 000000000000..be7047f2220f --- /dev/null +++ b/packages/camera/camera/test/camera_image_stream_test.dart @@ -0,0 +1,187 @@ +import 'package:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'camera_test.dart'; +import 'utils/method_channel_mock.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + CameraPlatform.instance = MockCameraPlatform(); + }); + + test('startImageStream() throws $CameraException when uninitialized', () { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + expect( + () => cameraController.startImageStream((image) => null), + throwsA(isA().having( + (error) => error.description, + 'Uninitialized CameraController.', + 'startImageStream was called on uninitialized CameraController.', + ))); + }); + + test('startImageStream() throws $CameraException when recording videos', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + + cameraController.value = + cameraController.value.copyWith(isRecordingVideo: true); + + expect( + () => cameraController.startImageStream((image) => null), + throwsA(isA().having( + (error) => error.description, + 'A video recording is already started.', + 'startImageStream was called while a video is being recorded.', + ))); + }); + test( + 'startImageStream() throws $CameraException when already streaming images', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + cameraController.value = + cameraController.value.copyWith(isStreamingImages: true); + expect( + () => cameraController.startImageStream((image) => null), + throwsA(isA().having( + (error) => error.description, + 'A camera has started streaming images.', + 'startImageStream was called while a camera was streaming images.', + ))); + }); + + test('startImageStream() calls CameraPlatform', () async { + MethodChannelMock cameraChannelMock = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'startImageStream': {}}); + MethodChannelMock streamChannelMock = MethodChannelMock( + channelName: 'plugins.flutter.io/camera/imageStream', + methods: {'listen': {}}); + + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.startImageStream((image) => null); + + expect(cameraChannelMock.log, + [isMethodCall('startImageStream', arguments: null)]); + expect(streamChannelMock.log, + [isMethodCall('listen', arguments: null)]); + }); + + test('stopImageStream() throws $CameraException when uninitialized', () { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + expect( + cameraController.stopImageStream, + throwsA(isA().having( + (error) => error.description, + 'Uninitialized CameraController.', + 'stopImageStream was called on uninitialized CameraController.', + ))); + }); + + test('stopImageStream() throws $CameraException when recording videos', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.startImageStream((image) => null); + cameraController.value = + cameraController.value.copyWith(isRecordingVideo: true); + expect( + cameraController.stopImageStream, + throwsA(isA().having( + (error) => error.description, + 'A video recording is already started.', + 'stopImageStream was called while a video is being recorded.', + ))); + }); + + test('stopImageStream() throws $CameraException when not streaming images', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + expect( + cameraController.stopImageStream, + throwsA(isA().having( + (error) => error.description, + 'No camera is streaming images', + 'stopImageStream was called when no camera is streaming images.', + ))); + }); + + test('stopImageStream() intended behaviour', () async { + MethodChannelMock cameraChannelMock = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'startImageStream': {}, 'stopImageStream': {}}); + MethodChannelMock streamChannelMock = MethodChannelMock( + channelName: 'plugins.flutter.io/camera/imageStream', + methods: {'listen': {}, 'cancel': {}}); + + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + await cameraController.startImageStream((image) => null); + await cameraController.stopImageStream(); + + expect(cameraChannelMock.log, [ + isMethodCall('startImageStream', arguments: null), + isMethodCall('stopImageStream', arguments: null) + ]); + + expect(streamChannelMock.log, [ + isMethodCall('listen', arguments: null), + isMethodCall('cancel', arguments: null) + ]); + }); +} diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart new file mode 100644 index 000000000000..c7f8e4320434 --- /dev/null +++ b/packages/camera/camera/test/camera_image_test.dart @@ -0,0 +1,114 @@ +// Copyright 2019 The Chromium 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:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('$CameraImage tests', () { + test('$CameraImage can be created', () { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + CameraImage cameraImage = CameraImage.fromPlatformData({ + 'format': 35, + 'height': 1, + 'width': 4, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.height, 1); + expect(cameraImage.width, 4); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + expect(cameraImage.planes.length, 1); + }); + + test('$CameraImage has ImageFormatGroup.yuv420 for iOS', () { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + + CameraImage cameraImage = CameraImage.fromPlatformData({ + 'format': 875704438, + 'height': 1, + 'width': 4, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + }); + + test('$CameraImage has ImageFormatGroup.yuv420 for Android', () { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + CameraImage cameraImage = CameraImage.fromPlatformData({ + 'format': 35, + 'height': 1, + 'width': 4, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + }); + + test('$CameraImage has ImageFormatGroup.bgra8888 for iOS', () { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + + CameraImage cameraImage = CameraImage.fromPlatformData({ + 'format': 1111970369, + 'height': 1, + 'width': 4, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.bgra8888); + }); + test('$CameraImage has ImageFormatGroup.unknown', () { + CameraImage cameraImage = CameraImage.fromPlatformData({ + 'format': null, + 'height': 1, + 'width': 4, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.unknown); + }); + }); +} diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index cc33b369f000..1cea609d1741 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -1,10 +1,56 @@ // Copyright 2017 The Chromium 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 'dart:math'; +import 'dart:ui'; + import 'package:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +get mockAvailableCameras => [ + CameraDescription( + name: 'camBack', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + CameraDescription( + name: 'camFront', + lensDirection: CameraLensDirection.front, + sensorOrientation: 180), + ]; + +get mockInitializeCamera => 13; + +get mockOnCameraInitializedEvent => CameraInitializedEvent( + 13, + 75, + 75, + ExposureMode.auto, + true, + FocusMode.auto, + true, + ); + +get mockOnCameraClosingEvent => null; + +get mockOnCameraErrorEvent => CameraErrorEvent(13, 'closing'); + +XFile mockTakePicture = XFile('foo/bar.png'); + +get mockVideoRecordingXFile => null; + +bool mockPlatformException = false; void main() { + WidgetsFlutterBinding.ensureInitialized(); + group('camera', () { test('debugCheckIsDisposed should not throw assertion error when disposed', () { @@ -32,7 +78,994 @@ void main() { throwsAssertionError, ); }); + + test('availableCameras() has camera', () async { + CameraPlatform.instance = MockCameraPlatform(); + + var camList = await availableCameras(); + + expect(camList, equals(mockAvailableCameras)); + }); }); + + group('$CameraController', () { + setUpAll(() { + CameraPlatform.instance = MockCameraPlatform(); + }); + + test('Can be initialized', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + expect(cameraController.value.aspectRatio, 1); + expect(cameraController.value.previewSize, Size(75, 75)); + expect(cameraController.value.isInitialized, isTrue); + }); + + test('can be disposed', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + expect(cameraController.value.aspectRatio, 1); + expect(cameraController.value.previewSize, Size(75, 75)); + expect(cameraController.value.isInitialized, isTrue); + + await cameraController.dispose(); + + verify(CameraPlatform.instance.dispose(13)).called(1); + }); + + test('initialize() throws CameraException when disposed', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + expect(cameraController.value.aspectRatio, 1); + expect(cameraController.value.previewSize, Size(75, 75)); + expect(cameraController.value.isInitialized, isTrue); + + await cameraController.dispose(); + + verify(CameraPlatform.instance.dispose(13)).called(1); + + expect( + cameraController.initialize, + throwsA(isA().having( + (error) => error.description, + 'Error description', + 'initialize was called on a disposed CameraController', + ))); + }); + + test('initialize() throws $CameraException on $PlatformException ', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + mockPlatformException = true; + + expect( + cameraController.initialize, + throwsA(isA().having( + (error) => error.description, + 'foo', + 'bar', + ))); + mockPlatformException = false; + }); + + test('initialize() sets imageFormat', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max, + imageFormatGroup: ImageFormatGroup.yuv420, + ); + await cameraController.initialize(); + verify(CameraPlatform.instance + .initializeCamera(13, imageFormatGroup: ImageFormatGroup.yuv420)) + .called(1); + }); + + test('prepareForVideoRecording() calls $CameraPlatform ', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.prepareForVideoRecording(); + + verify(CameraPlatform.instance.prepareForVideoRecording()).called(1); + }); + + test('takePicture() throws $CameraException when uninitialized ', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + expect( + cameraController.takePicture(), + throwsA(isA().having( + (error) => error.description, + 'Uninitialized CameraController.', + 'takePicture was called on uninitialized CameraController', + ))); + }); + + test('takePicture() throws $CameraException when takePicture is true', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + cameraController.value = + cameraController.value.copyWith(isTakingPicture: true); + expect( + cameraController.takePicture(), + throwsA(isA().having( + (error) => error.description, + 'Previous capture has not returned yet.', + 'takePicture was called before the previous capture returned.', + ))); + }); + + test('takePicture() returns $XFile', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + XFile xFile = await cameraController.takePicture(); + + expect(xFile.path, mockTakePicture.path); + }); + + test('takePicture() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + mockPlatformException = true; + expect( + cameraController.takePicture(), + throwsA(isA().having( + (error) => error.description, + 'foo', + 'bar', + ))); + mockPlatformException = false; + }); + + test('startVideoRecording() throws $CameraException when uninitialized', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + expect( + cameraController.startVideoRecording(), + throwsA(isA().having( + (error) => error.description, + 'Uninitialized CameraController', + 'startVideoRecording was called on uninitialized CameraController', + ))); + }); + test('startVideoRecording() throws $CameraException when recording videos', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + + cameraController.value = + cameraController.value.copyWith(isRecordingVideo: true); + + expect( + cameraController.startVideoRecording(), + throwsA(isA().having( + (error) => error.description, + 'A video recording is already started.', + 'startVideoRecording was called when a recording is already started.', + ))); + }); + + test( + 'startVideoRecording() throws $CameraException when already streaming images', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + + cameraController.value = + cameraController.value.copyWith(isStreamingImages: true); + + expect( + cameraController.startVideoRecording(), + throwsA(isA().having( + (error) => error.description, + 'A camera has started streaming images.', + 'startVideoRecording was called while a camera was streaming images.', + ))); + }); + + test('getMaxZoomLevel() throws $CameraException when uninitialized', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + expect( + cameraController.getMaxZoomLevel, + throwsA(isA().having( + (error) => error.description, + 'Uninitialized CameraController', + 'getMaxZoomLevel was called on uninitialized CameraController', + ))); + }); + + test('getMaxZoomLevel() throws $CameraException when disposed', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + await cameraController.dispose(); + + expect( + cameraController.getMaxZoomLevel, + throwsA(isA().having( + (error) => error.description, + 'Uninitialized CameraController', + 'getMaxZoomLevel was called on uninitialized CameraController', + ))); + }); + + test( + 'getMaxZoomLevel() throws $CameraException when a platform exception occured.', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance.getMaxZoomLevel(mockInitializeCamera)) + .thenThrow(PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error messge', + details: null)); + + expect( + cameraController.getMaxZoomLevel, + throwsA(isA() + .having((error) => error.code, 'code', 'TEST_ERROR') + .having( + (error) => error.description, + 'description', + 'This is a test error messge', + ))); + }); + + test('getMaxZoomLevel() returns max zoom level.', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance.getMaxZoomLevel(mockInitializeCamera)) + .thenAnswer((_) => Future.value(42.0)); + + final maxZoomLevel = await cameraController.getMaxZoomLevel(); + expect(maxZoomLevel, 42.0); + }); + + test('getMinZoomLevel() throws $CameraException when uninitialized', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + expect( + cameraController.getMinZoomLevel, + throwsA(isA().having( + (error) => error.description, + 'Uninitialized CameraController', + 'getMinZoomLevel was called on uninitialized CameraController', + ))); + }); + + test('getMinZoomLevel() throws $CameraException when disposed', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + await cameraController.dispose(); + + expect( + cameraController.getMinZoomLevel, + throwsA(isA().having( + (error) => error.description, + 'Uninitialized CameraController', + 'getMinZoomLevel was called on uninitialized CameraController', + ))); + }); + + test( + 'getMinZoomLevel() throws $CameraException when a platform exception occured.', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance.getMinZoomLevel(mockInitializeCamera)) + .thenThrow(PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error messge', + details: null)); + + expect( + cameraController.getMinZoomLevel, + throwsA(isA() + .having((error) => error.code, 'code', 'TEST_ERROR') + .having( + (error) => error.description, + 'description', + 'This is a test error messge', + ))); + }); + + test('getMinZoomLevel() returns max zoom level.', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance.getMinZoomLevel(mockInitializeCamera)) + .thenAnswer((_) => Future.value(42.0)); + + final maxZoomLevel = await cameraController.getMinZoomLevel(); + expect(maxZoomLevel, 42.0); + }); + + test('setZoomLevel() throws $CameraException when uninitialized', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + expect( + () => cameraController.setZoomLevel(42.0), + throwsA(isA().having( + (error) => error.description, + 'Uninitialized CameraController', + 'setZoomLevel was called on uninitialized CameraController', + ))); + }); + + test('setZoomLevel() throws $CameraException when disposed', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + await cameraController.dispose(); + + expect( + () => cameraController.setZoomLevel(42.0), + throwsA(isA().having( + (error) => error.description, + 'Uninitialized CameraController', + 'setZoomLevel was called on uninitialized CameraController', + ))); + }); + + test( + 'setZoomLevel() throws $CameraException when a platform exception occured.', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance.setZoomLevel(mockInitializeCamera, 42.0)) + .thenThrow(PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error messge', + details: null)); + + expect( + () => cameraController.setZoomLevel(42), + throwsA(isA() + .having((error) => error.code, 'code', 'TEST_ERROR') + .having( + (error) => error.description, + 'description', + 'This is a test error messge', + ))); + + reset(CameraPlatform.instance); + }); + + test( + 'setZoomLevel() completes and calls method channel with correct value.', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + await cameraController.setZoomLevel(42.0); + + verify(CameraPlatform.instance.setZoomLevel(mockInitializeCamera, 42.0)) + .called(1); + }); + + test('setFlashMode() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.setFlashMode(FlashMode.always); + + verify(CameraPlatform.instance + .setFlashMode(cameraController.cameraId, FlashMode.always)) + .called(1); + }); + + test('setFlashMode() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .setFlashMode(cameraController.cameraId, FlashMode.always)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.setFlashMode(FlashMode.always), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('setExposureMode() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.setExposureMode(ExposureMode.auto); + + verify(CameraPlatform.instance + .setExposureMode(cameraController.cameraId, ExposureMode.auto)) + .called(1); + }); + + test('setExposureMode() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .setExposureMode(cameraController.cameraId, ExposureMode.auto)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.setExposureMode(ExposureMode.auto), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('setExposurePoint() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.setExposurePoint(Offset(0.5, 0.5)); + + verify(CameraPlatform.instance.setExposurePoint( + cameraController.cameraId, Point(0.5, 0.5))) + .called(1); + }); + + test('setExposurePoint() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance.setExposurePoint( + cameraController.cameraId, Point(0.5, 0.5))) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.setExposurePoint(Offset(0.5, 0.5)), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('getMinExposureOffset() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.getMinExposureOffset(); + + verify(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .called(1); + }); + + test('getMinExposureOffset() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.getMinExposureOffset(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('getMaxExposureOffset() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.getMaxExposureOffset(); + + verify(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .called(1); + }); + + test('getMaxExposureOffset() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.getMaxExposureOffset(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('getExposureOffsetStepSize() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.getExposureOffsetStepSize(); + + verify(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .called(1); + }); + + test( + 'getExposureOffsetStepSize() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.getExposureOffsetStepSize(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('setExposureOffset() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => -1.0); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 2.0); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 1.0); + + await cameraController.setExposureOffset(1.0); + + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 1.0)) + .called(1); + }); + + test('setExposureOffset() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => -1.0); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 2.0); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 1.0); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 1.0)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.setExposureOffset(1.0), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test( + 'setExposureOffset() throws $CameraException when offset is out of bounds', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => -1.0); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 2.0); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 1.0); + + expect( + cameraController.setExposureOffset(3.0), + throwsA(isA().having( + (error) => error.description, + 'exposureOffsetOutOfBounds', + 'The provided exposure offset was outside the supported range for this device.', + ))); + expect( + cameraController.setExposureOffset(-2.0), + throwsA(isA().having( + (error) => error.description, + 'exposureOffsetOutOfBounds', + 'The provided exposure offset was outside the supported range for this device.', + ))); + + await cameraController.setExposureOffset(2.0); + await cameraController.setExposureOffset(-1.0); + await cameraController.setExposureOffset(-0.0); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 2.0)) + .called(1); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -1.0)) + .called(1); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.0)) + .called(1); + }); + + test('setExposureOffset() rounds offset to nearest step', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => -1.0); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 1.0); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 0.4); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 1.0)) + .thenAnswer((_) async => 1.0); + + await cameraController.setExposureOffset(1.0); + await cameraController.setExposureOffset(-1.0); + await cameraController.setExposureOffset(0.1); + await cameraController.setExposureOffset(0.2); + await cameraController.setExposureOffset(0.3); + await cameraController.setExposureOffset(0.4); + await cameraController.setExposureOffset(0.5); + await cameraController.setExposureOffset(0.6); + await cameraController.setExposureOffset(0.7); + await cameraController.setExposureOffset(-0.1); + await cameraController.setExposureOffset(-0.2); + await cameraController.setExposureOffset(-0.3); + await cameraController.setExposureOffset(-0.4); + await cameraController.setExposureOffset(-0.5); + await cameraController.setExposureOffset(-0.6); + await cameraController.setExposureOffset(-0.7); + + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.8)) + .called(3); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -0.8)) + .called(3); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.0)) + .called(2); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.4)) + .called(4); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -0.4)) + .called(4); + }); + }); +} + +class MockCameraPlatform extends Mock + with MockPlatformInterfaceMixin + implements CameraPlatform { + @override + Future initializeCamera(int cameraId, + {ImageFormatGroup imageFormatGroup}); + + @override + Future> availableCameras() => + Future.value(mockAvailableCameras); + + @override + Future createCamera( + CameraDescription description, + ResolutionPreset resolutionPreset, { + bool enableAudio, + }) => + mockPlatformException + ? throw PlatformException(code: 'foo', message: 'bar') + : Future.value(mockInitializeCamera); + + @override + Stream onCameraInitialized(int cameraId) => + Stream.value(mockOnCameraInitializedEvent); + + @override + Stream onCameraClosing(int cameraId) => + Stream.value(mockOnCameraClosingEvent); + + @override + Stream onCameraError(int cameraId) => + Stream.value(mockOnCameraErrorEvent); + + @override + Future takePicture(int cameraId) => mockPlatformException + ? throw PlatformException(code: 'foo', message: 'bar') + : Future.value(mockTakePicture); + + @override + Future startVideoRecording(int cameraId, + {Duration maxVideoDuration}) => + Future.value(mockVideoRecordingXFile); } class MockCameraDescription extends CameraDescription { diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart new file mode 100644 index 000000000000..eb7927b9eb6c --- /dev/null +++ b/packages/camera/camera/test/camera_value_test.dart @@ -0,0 +1,119 @@ +// Copyright 2019 The Chromium 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:ui'; + +import 'package:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('camera_value', () { + test('Can be created', () { + var cameraValue = const CameraValue( + isInitialized: false, + errorDescription: null, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + exposurePointSupported: true); + + expect(cameraValue, isA()); + expect(cameraValue.isInitialized, isFalse); + expect(cameraValue.errorDescription, null); + expect(cameraValue.previewSize, Size(10, 10)); + expect(cameraValue.isRecordingPaused, isFalse); + expect(cameraValue.isRecordingVideo, isFalse); + expect(cameraValue.isTakingPicture, isFalse); + expect(cameraValue.isStreamingImages, isFalse); + expect(cameraValue.flashMode, FlashMode.auto); + expect(cameraValue.exposureMode, ExposureMode.auto); + expect(cameraValue.exposurePointSupported, true); + }); + + test('Can be created as uninitialized', () { + var cameraValue = const CameraValue.uninitialized(); + + expect(cameraValue, isA()); + expect(cameraValue.isInitialized, isFalse); + expect(cameraValue.errorDescription, null); + expect(cameraValue.previewSize, null); + expect(cameraValue.isRecordingPaused, isFalse); + expect(cameraValue.isRecordingVideo, isFalse); + expect(cameraValue.isTakingPicture, isFalse); + expect(cameraValue.isStreamingImages, isFalse); + expect(cameraValue.flashMode, FlashMode.auto); + expect(cameraValue.exposureMode, null); + expect(cameraValue.exposurePointSupported, false); + }); + + test('Can be copied with isInitialized', () { + var cv = const CameraValue.uninitialized(); + var cameraValue = cv.copyWith(isInitialized: true); + + expect(cameraValue, isA()); + expect(cameraValue.isInitialized, isTrue); + expect(cameraValue.errorDescription, null); + expect(cameraValue.previewSize, null); + expect(cameraValue.isRecordingPaused, isFalse); + expect(cameraValue.isRecordingVideo, isFalse); + expect(cameraValue.isTakingPicture, isFalse); + expect(cameraValue.isStreamingImages, isFalse); + expect(cameraValue.flashMode, FlashMode.auto); + expect(cameraValue.exposureMode, null); + expect(cameraValue.exposurePointSupported, false); + }); + + test('Has aspectRatio after setting size', () { + var cv = const CameraValue.uninitialized(); + var cameraValue = + cv.copyWith(isInitialized: true, previewSize: Size(20, 10)); + + expect(cameraValue.aspectRatio, 0.5); + }); + + test('hasError is true after setting errorDescription', () { + var cv = const CameraValue.uninitialized(); + var cameraValue = cv.copyWith(errorDescription: 'error'); + + expect(cameraValue.hasError, isTrue); + expect(cameraValue.errorDescription, 'error'); + }); + + test('Recording paused is false when not recording', () { + var cv = const CameraValue.uninitialized(); + var cameraValue = cv.copyWith( + isInitialized: true, + isRecordingVideo: false, + isRecordingPaused: true); + + expect(cameraValue.isRecordingPaused, isFalse); + }); + + test('toString() works as expected', () { + var cameraValue = const CameraValue( + isInitialized: false, + errorDescription: null, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + focusMode: FocusMode.auto, + exposurePointSupported: true, + focusPointSupported: true, + ); + + expect(cameraValue.toString(), + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true)'); + }); + }); +} diff --git a/packages/camera/camera/test/utils/method_channel_mock.dart b/packages/camera/camera/test/utils/method_channel_mock.dart new file mode 100644 index 000000000000..cdf393f82b5f --- /dev/null +++ b/packages/camera/camera/test/utils/method_channel_mock.dart @@ -0,0 +1,38 @@ +// Copyright 2019 The Chromium 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/services.dart'; + +class MethodChannelMock { + final Duration delay; + final MethodChannel methodChannel; + final Map methods; + final log = []; + + MethodChannelMock({ + String channelName, + this.delay, + this.methods, + }) : methodChannel = MethodChannel(channelName) { + methodChannel.setMockMethodCallHandler(_handler); + } + + Future _handler(MethodCall methodCall) async { + log.add(methodCall); + + if (!methods.containsKey(methodCall.method)) { + throw MissingPluginException('No implementation found for method ' + '${methodCall.method} on channel ${methodChannel.name}'); + } + + return Future.delayed(delay ?? Duration.zero, () { + final result = methods[methodCall.method]; + if (result is Exception) { + throw result; + } + + return Future.value(result); + }); + } +} diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..d7442d4ac931 --- /dev/null +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -0,0 +1,40 @@ +## 1.5.0 + +- Introduces interface methods for locking and unlocking the capture orientation. +- Introduces interface method for listening to the device orientation. + +## 1.4.0 + +- Added interface methods to support auto focus. + +## 1.3.0 + +- Introduces an option to set the image format when initializing. + +## 1.2.0 + +- Added interface to support automatic exposure. + +## 1.1.0 + +- Added an optional `maxVideoDuration` parameter to the `startVideoRecording` method, which allows implementations to limit the duration of a video recording. + +## 1.0.4 + +- Added the torch option to the FlashMode enum, which when implemented indicates the flash light should be turned on continuously. + +## 1.0.3 + +- Update Flutter SDK constraint. + +## 1.0.2 + +- Added interface methods to support zoom features. + +## 1.0.1 + +- Added interface methods for setting flash mode. + +## 1.0.0 + +- Initial open-source release diff --git a/packages/camera/camera_platform_interface/LICENSE b/packages/camera/camera_platform_interface/LICENSE new file mode 100644 index 000000000000..a6d6c0749818 --- /dev/null +++ b/packages/camera/camera_platform_interface/LICENSE @@ -0,0 +1,25 @@ +Copyright 2017 The Chromium 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/camera/camera_platform_interface/README.md b/packages/camera/camera_platform_interface/README.md new file mode 100644 index 000000000000..43be651935b5 --- /dev/null +++ b/packages/camera/camera_platform_interface/README.md @@ -0,0 +1,26 @@ +# camera_platform_interface + +A common platform interface for the [`camera`][1] plugin. + +This interface allows platform-specific implementations of the `camera` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `camera`, extend +[`CameraPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`CameraPlatform` by calling +`CameraPlatform.instance = MyPlatformCamera()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../camera +[2]: lib/camera_platform_interface.dart diff --git a/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart b/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart new file mode 100644 index 000000000000..d7e5fcd0834c --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart @@ -0,0 +1,11 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/events/camera_event.dart'; +export 'src/events/device_event.dart'; +export 'src/platform_interface/camera_platform.dart'; +export 'src/types/types.dart'; + +/// Expose XFile +export 'package:cross_file/cross_file.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart new file mode 100644 index 000000000000..d60a0a39f608 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart @@ -0,0 +1,237 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/src/types/focus_mode.dart'; + +import '../../camera_platform_interface.dart'; + +/// Generic Event coming from the native side of Camera, +/// related to a specific camera module. +/// +/// All [CameraEvent]s contain the `cameraId` that originated the event. This +/// should never be `null`. +/// +/// This class is used as a base class for all the events that might be +/// triggered from a Camera, but it is never used directly as an event type. +/// +/// Do NOT instantiate new events like `CameraEvent(cameraId)` directly, +/// use a specific class instead: +/// +/// Do `class NewEvent extend CameraEvent` when creating your own events. +/// See below for examples: `CameraClosingEvent`, `CameraErrorEvent`... +/// These events are more semantic and more pleasant to use than raw generics. +/// They can be (and in fact, are) filtered by the `instanceof`-operator. +abstract class CameraEvent { + /// The ID of the Camera this event is associated to. + final int cameraId; + + /// Build a Camera Event, that relates a `cameraId`. + /// + /// The `cameraId` is the ID of the camera that triggered the event. + CameraEvent(this.cameraId) : assert(cameraId != null); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CameraEvent && + runtimeType == other.runtimeType && + cameraId == other.cameraId; + + @override + int get hashCode => cameraId.hashCode; +} + +/// An event fired when the camera has finished initializing. +class CameraInitializedEvent extends CameraEvent { + /// The width of the preview in pixels. + final double previewWidth; + + /// The height of the preview in pixels. + final double previewHeight; + + /// The default exposure mode + final ExposureMode exposureMode; + + /// The default focus mode + final FocusMode focusMode; + + /// Whether setting exposure points is supported. + final bool exposurePointSupported; + + /// Whether setting focus points is supported. + final bool focusPointSupported; + + /// Build a CameraInitialized event triggered from the camera represented by + /// `cameraId`. + /// + /// The `previewWidth` represents the width of the generated preview in pixels. + /// The `previewHeight` represents the height of the generated preview in pixels. + CameraInitializedEvent( + int cameraId, + this.previewWidth, + this.previewHeight, [ + this.exposureMode, + this.exposurePointSupported = false, + this.focusMode, + this.focusPointSupported = false, + ]) : super(cameraId); + + /// Converts the supplied [Map] to an instance of the [CameraInitializedEvent] + /// class. + CameraInitializedEvent.fromJson(Map json) + : previewWidth = json['previewWidth'], + previewHeight = json['previewHeight'], + exposureMode = deserializeExposureMode(json['exposureMode']), + exposurePointSupported = json['exposurePointSupported'] ?? false, + focusMode = deserializeFocusMode(json['focusMode']), + focusPointSupported = json['focusPointSupported'] ?? false, + super(json['cameraId']); + + /// Converts the [CameraInitializedEvent] instance into a [Map] instance that + /// can be serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + 'previewWidth': previewWidth, + 'previewHeight': previewHeight, + 'exposureMode': serializeExposureMode(exposureMode), + 'exposurePointSupported': exposurePointSupported, + 'focusMode': serializeFocusMode(focusMode), + 'focusPointSupported': focusPointSupported, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + super == other && + other is CameraInitializedEvent && + runtimeType == other.runtimeType && + previewWidth == other.previewWidth && + previewHeight == other.previewHeight && + exposureMode == other.exposureMode && + exposurePointSupported == other.exposurePointSupported && + focusMode == other.focusMode && + focusPointSupported == other.focusPointSupported; + + @override + int get hashCode => + super.hashCode ^ + previewWidth.hashCode ^ + previewHeight.hashCode ^ + exposureMode.hashCode ^ + exposurePointSupported.hashCode ^ + focusMode.hashCode ^ + focusPointSupported.hashCode; +} + +/// An event fired when the resolution preset of the camera has changed. +class CameraResolutionChangedEvent extends CameraEvent { + /// The capture width in pixels. + final double captureWidth; + + /// The capture height in pixels. + final double captureHeight; + + /// Build a CameraResolutionChanged event triggered from the camera + /// represented by `cameraId`. + /// + /// The `captureWidth` represents the width of the resulting image in pixels. + /// The `captureHeight` represents the height of the resulting image in pixels. + CameraResolutionChangedEvent( + int cameraId, + this.captureWidth, + this.captureHeight, + ) : super(cameraId); + + /// Converts the supplied [Map] to an instance of the + /// [CameraResolutionChangedEvent] class. + CameraResolutionChangedEvent.fromJson(Map json) + : captureWidth = json['captureWidth'], + captureHeight = json['captureHeight'], + super(json['cameraId']); + + /// Converts the [CameraResolutionChangedEvent] instance into a [Map] instance + /// that can be serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + 'captureWidth': captureWidth, + 'captureHeight': captureHeight, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CameraResolutionChangedEvent && + super == (other) && + runtimeType == other.runtimeType && + captureWidth == other.captureWidth && + captureHeight == other.captureHeight; + + @override + int get hashCode => + super.hashCode ^ captureWidth.hashCode ^ captureHeight.hashCode; +} + +/// An event fired when the camera is going to close. +class CameraClosingEvent extends CameraEvent { + /// Build a CameraClosing event triggered from the camera represented by + /// `cameraId`. + CameraClosingEvent(int cameraId) : super(cameraId); + + /// Converts the supplied [Map] to an instance of the [CameraClosingEvent] + /// class. + CameraClosingEvent.fromJson(Map json) + : super(json['cameraId']); + + /// Converts the [CameraClosingEvent] instance into a [Map] instance that can + /// be serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + super == (other) && + other is CameraClosingEvent && + runtimeType == other.runtimeType; + + @override + int get hashCode => super.hashCode; +} + +/// An event fired when an error occured while operating the camera. +class CameraErrorEvent extends CameraEvent { + /// Description of the error. + final String description; + + /// Build a CameraError event triggered from the camera represented by + /// `cameraId`. + /// + /// The `description` represents the error occured on the camera. + CameraErrorEvent(int cameraId, this.description) : super(cameraId); + + /// Converts the supplied [Map] to an instance of the [CameraErrorEvent] + /// class. + CameraErrorEvent.fromJson(Map json) + : description = json['description'], + super(json['cameraId']); + + /// Converts the [CameraErrorEvent] instance into a [Map] instance that can be + /// serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + 'description': description, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + super == (other) && + other is CameraErrorEvent && + runtimeType == other.runtimeType && + description == other.description; + + @override + int get hashCode => super.hashCode ^ description.hashCode; +} diff --git a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart new file mode 100644 index 000000000000..82febcab2290 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart @@ -0,0 +1,52 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/src/utils/utils.dart'; +import 'package:flutter/services.dart'; + +/// Generic Event coming from the native side of Camera, +/// not related to a specific camera module. +/// +/// This class is used as a base class for all the events that might be +/// triggered from a device, but it is never used directly as an event type. +/// +/// Do NOT instantiate new events like `DeviceEvent()` directly, +/// use a specific class instead: +/// +/// Do `class NewEvent extend DeviceEvent` when creating your own events. +/// See below for examples: `DeviceOrientationChangedEvent`... +/// These events are more semantic and more pleasant to use than raw generics. +/// They can be (and in fact, are) filtered by the `instanceof`-operator. +abstract class DeviceEvent {} + +/// The [DeviceOrientationChangedEvent] is fired every time the user changes the +/// physical orientation of the device. +class DeviceOrientationChangedEvent extends DeviceEvent { + /// The new orientation of the device + final DeviceOrientation orientation; + + /// Build a new orientation changed event. + DeviceOrientationChangedEvent(this.orientation); + + /// Converts the supplied [Map] to an instance of the [DeviceOrientationChangedEvent] + /// class. + DeviceOrientationChangedEvent.fromJson(Map json) + : orientation = deserializeDeviceOrientation(json['orientation']); + + /// Converts the [DeviceOrientationChangedEvent] instance into a [Map] instance that + /// can be serialized to JSON. + Map toJson() => { + 'orientation': serializeDeviceOrientation(orientation), + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DeviceOrientationChangedEvent && + runtimeType == other.runtimeType && + orientation == other.orientation; + + @override + int get hashCode => orientation.hashCode; +} diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart new file mode 100644 index 000000000000..e6f658c45365 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -0,0 +1,446 @@ +// Copyright 2018 The Chromium 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 'dart:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/events/device_event.dart'; +import 'package:camera_platform_interface/src/types/focus_mode.dart'; +import 'package:camera_platform_interface/src/types/image_format_group.dart'; +import 'package:camera_platform_interface/src/utils/utils.dart'; +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; +import 'package:stream_transform/stream_transform.dart'; + +const MethodChannel _channel = MethodChannel('plugins.flutter.io/camera'); + +/// An implementation of [CameraPlatform] that uses method channels. +class MethodChannelCamera extends CameraPlatform { + final Map _channels = {}; + + /// The controller we need to broadcast the different events coming + /// from handleMethodCall, specific to camera events. + /// + /// It is a `broadcast` because multiple controllers will connect to + /// different stream views of this Controller. + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + final StreamController cameraEventStreamController = + StreamController.broadcast(); + + /// The controller we need to broadcast the different events coming + /// from handleMethodCall, specific to general device events. + /// + /// It is a `broadcast` because multiple controllers will connect to + /// different stream views of this Controller. + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + final StreamController deviceEventStreamController = + StreamController.broadcast(); + + Stream _cameraEvents(int cameraId) => + cameraEventStreamController.stream + .where((event) => event.cameraId == cameraId); + + /// Construct a new method channel camera instance. + MethodChannelCamera() { + final channel = MethodChannel('flutter.io/cameraPlugin/device'); + channel.setMethodCallHandler( + (MethodCall call) => handleDeviceMethodCall(call)); + } + + @override + Future> availableCameras() async { + try { + final List> cameras = await _channel + .invokeListMethod>('availableCameras'); + return cameras.map((Map camera) { + return CameraDescription( + name: camera['name'], + lensDirection: parseCameraLensDirection(camera['lensFacing']), + sensorOrientation: camera['sensorOrientation'], + ); + }).toList(); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset resolutionPreset, { + bool enableAudio, + }) async { + try { + final Map reply = + await _channel.invokeMapMethod( + 'create', + { + 'cameraName': cameraDescription.name, + 'resolutionPreset': resolutionPreset != null + ? _serializeResolutionPreset(resolutionPreset) + : null, + 'enableAudio': enableAudio, + }, + ); + return reply['cameraId']; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future initializeCamera(int cameraId, + {ImageFormatGroup imageFormatGroup}) { + _channels.putIfAbsent(cameraId, () { + final channel = MethodChannel('flutter.io/cameraPlugin/camera$cameraId'); + channel.setMethodCallHandler( + (MethodCall call) => handleCameraMethodCall(call, cameraId)); + return channel; + }); + + Completer _completer = Completer(); + + onCameraInitialized(cameraId).first.then((value) { + _completer.complete(); + }); + + _channel.invokeMapMethod( + 'initialize', + { + 'cameraId': cameraId, + 'imageFormatGroup': imageFormatGroup.name(), + }, + ); + + return _completer.future; + } + + @override + Future dispose(int cameraId) async { + await _channel.invokeMethod( + 'dispose', + {'cameraId': cameraId}, + ); + + if (_channels.containsKey(cameraId)) { + _channels[cameraId].setMethodCallHandler(null); + _channels.remove(cameraId); + } + } + + @override + Stream onCameraInitialized(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraResolutionChanged(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraClosing(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraError(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onDeviceOrientationChanged() { + return deviceEventStreamController.stream + .whereType(); + } + + @override + Future lockCaptureOrientation( + int cameraId, DeviceOrientation orientation) async { + await _channel.invokeMethod( + 'lockCaptureOrientation', + { + 'cameraId': cameraId, + 'orientation': serializeDeviceOrientation(orientation) + }, + ); + } + + @override + Future unlockCaptureOrientation(int cameraId) async { + await _channel.invokeMethod( + 'unlockCaptureOrientation', + {'cameraId': cameraId}, + ); + } + + @override + Future takePicture(int cameraId) async { + String path = await _channel.invokeMethod( + 'takePicture', + {'cameraId': cameraId}, + ); + return XFile(path); + } + + @override + Future prepareForVideoRecording() => + _channel.invokeMethod('prepareForVideoRecording'); + + @override + Future startVideoRecording(int cameraId, + {Duration maxVideoDuration}) async { + await _channel.invokeMethod( + 'startVideoRecording', + { + 'cameraId': cameraId, + 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + }, + ); + } + + @override + Future stopVideoRecording(int cameraId) async { + String path = await _channel.invokeMethod( + 'stopVideoRecording', + {'cameraId': cameraId}, + ); + return XFile(path); + } + + @override + Future pauseVideoRecording(int cameraId) => _channel.invokeMethod( + 'pauseVideoRecording', + {'cameraId': cameraId}, + ); + + @override + Future resumeVideoRecording(int cameraId) => + _channel.invokeMethod( + 'resumeVideoRecording', + {'cameraId': cameraId}, + ); + + @override + Future setFlashMode(int cameraId, FlashMode mode) => + _channel.invokeMethod( + 'setFlashMode', + { + 'cameraId': cameraId, + 'mode': _serializeFlashMode(mode), + }, + ); + + @override + Future setExposureMode(int cameraId, ExposureMode mode) => + _channel.invokeMethod( + 'setExposureMode', + { + 'cameraId': cameraId, + 'mode': serializeExposureMode(mode), + }, + ); + + @override + Future setExposurePoint(int cameraId, Point point) { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + return _channel.invokeMethod( + 'setExposurePoint', + { + 'cameraId': cameraId, + 'reset': point == null, + 'x': point?.x, + 'y': point?.y, + }, + ); + } + + @override + Future getMinExposureOffset(int cameraId) => + _channel.invokeMethod( + 'getMinExposureOffset', + {'cameraId': cameraId}, + ); + + @override + Future getMaxExposureOffset(int cameraId) => + _channel.invokeMethod( + 'getMaxExposureOffset', + {'cameraId': cameraId}, + ); + + @override + Future getExposureOffsetStepSize(int cameraId) => + _channel.invokeMethod( + 'getExposureOffsetStepSize', + {'cameraId': cameraId}, + ); + + @override + Future setExposureOffset(int cameraId, double offset) => + _channel.invokeMethod( + 'setExposureOffset', + { + 'cameraId': cameraId, + 'offset': offset, + }, + ); + + @override + Future setFocusMode(int cameraId, FocusMode mode) => + _channel.invokeMethod( + 'setFocusMode', + { + 'cameraId': cameraId, + 'mode': serializeFocusMode(mode), + }, + ); + + @override + Future setFocusPoint(int cameraId, Point point) { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + return _channel.invokeMethod( + 'setFocusPoint', + { + 'cameraId': cameraId, + 'reset': point == null, + 'x': point?.x, + 'y': point?.y, + }, + ); + } + + @override + Future getMaxZoomLevel(int cameraId) => _channel.invokeMethod( + 'getMaxZoomLevel', + {'cameraId': cameraId}, + ); + + @override + Future getMinZoomLevel(int cameraId) => _channel.invokeMethod( + 'getMinZoomLevel', + {'cameraId': cameraId}, + ); + + @override + Future setZoomLevel(int cameraId, double zoom) async { + try { + await _channel.invokeMethod( + 'setZoomLevel', + { + 'cameraId': cameraId, + 'zoom': zoom, + }, + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Widget buildPreview(int cameraId) { + return Texture(textureId: cameraId); + } + + /// Returns the flash mode as a String. + String _serializeFlashMode(FlashMode flashMode) { + switch (flashMode) { + case FlashMode.off: + return 'off'; + case FlashMode.auto: + return 'auto'; + case FlashMode.always: + return 'always'; + case FlashMode.torch: + return 'torch'; + default: + throw ArgumentError('Unknown FlashMode value'); + } + } + + /// Returns the resolution preset as a String. + String _serializeResolutionPreset(ResolutionPreset resolutionPreset) { + switch (resolutionPreset) { + case ResolutionPreset.max: + return 'max'; + case ResolutionPreset.ultraHigh: + return 'ultraHigh'; + case ResolutionPreset.veryHigh: + return 'veryHigh'; + case ResolutionPreset.high: + return 'high'; + case ResolutionPreset.medium: + return 'medium'; + case ResolutionPreset.low: + return 'low'; + default: + throw ArgumentError('Unknown ResolutionPreset value'); + } + } + + /// Converts messages received from the native platform into device events. + /// + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + Future handleDeviceMethodCall(MethodCall call) async { + switch (call.method) { + case 'orientation_changed': + deviceEventStreamController.add(DeviceOrientationChangedEvent( + deserializeDeviceOrientation(call.arguments['orientation']))); + break; + default: + throw MissingPluginException(); + } + } + + /// Converts messages received from the native platform into camera events. + /// + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + Future handleCameraMethodCall(MethodCall call, int cameraId) async { + switch (call.method) { + case 'initialized': + cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + call.arguments['previewWidth'], + call.arguments['previewHeight'], + deserializeExposureMode(call.arguments['exposureMode']), + call.arguments['exposurePointSupported'], + deserializeFocusMode(call.arguments['focusMode']), + call.arguments['focusPointSupported'], + )); + break; + case 'resolution_changed': + cameraEventStreamController.add(CameraResolutionChangedEvent( + cameraId, + call.arguments['captureWidth'], + call.arguments['captureHeight'], + )); + break; + case 'camera_closing': + cameraEventStreamController.add(CameraClosingEvent( + cameraId, + )); + break; + case 'error': + cameraEventStreamController.add(CameraErrorEvent( + cameraId, + call.arguments['description'], + )); + break; + default: + throw MissingPluginException(); + } + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart new file mode 100644 index 000000000000..c1d6e09c3263 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -0,0 +1,231 @@ +// Copyright 2018 The Chromium 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 'dart:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/events/device_event.dart'; +import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; +import 'package:camera_platform_interface/src/types/exposure_mode.dart'; +import 'package:camera_platform_interface/src/types/focus_mode.dart'; +import 'package:camera_platform_interface/src/types/image_format_group.dart'; +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// The interface that implementations of camera must implement. +/// +/// Platform implementations should extend this class rather than implement it as `camera` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [CameraPlatform] methods. +abstract class CameraPlatform extends PlatformInterface { + /// Constructs a CameraPlatform. + CameraPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CameraPlatform _instance = MethodChannelCamera(); + + /// The default instance of [CameraPlatform] to use. + /// + /// Defaults to [MethodChannelCamera]. + static CameraPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [CameraPlatform] when they register themselves. + static set instance(CameraPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Completes with a list of available cameras. + Future> availableCameras() { + throw UnimplementedError('availableCameras() is not implemented.'); + } + + /// Creates an uninitialized camera instance and returns the cameraId. + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset resolutionPreset, { + bool enableAudio, + }) { + throw UnimplementedError('createCamera() is not implemented.'); + } + + /// Initializes the camera on the device. + /// + /// [imageFormatGroup] is used to specify the image formatting used. + /// On Android this defaults to ImageFormat.YUV_420_888 and applies only to the imageStream. + /// On iOS this defaults to kCVPixelFormatType_32BGRA. + Future initializeCamera(int cameraId, + {ImageFormatGroup imageFormatGroup}) { + throw UnimplementedError('initializeCamera() is not implemented.'); + } + + /// The camera has been initialized + Stream onCameraInitialized(int cameraId) { + throw UnimplementedError('onCameraInitialized() is not implemented.'); + } + + /// The camera's resolution has changed + Stream onCameraResolutionChanged(int cameraId) { + throw UnimplementedError('onResolutionChanged() is not implemented.'); + } + + /// The camera started to close. + Stream onCameraClosing(int cameraId) { + throw UnimplementedError('onCameraClosing() is not implemented.'); + } + + /// The camera experienced an error. + Stream onCameraError(int cameraId) { + throw UnimplementedError('onCameraError() is not implemented.'); + } + + /// The device orientation changed. + /// + /// Implementations for this: + /// - Should support all 4 orientations. + /// - Should not emit new values when the screen orientation is locked. + Stream onDeviceOrientationChanged() { + throw UnimplementedError( + 'onDeviceOrientationChanged() is not implemented.'); + } + + /// Locks the capture orientation. + Future lockCaptureOrientation( + int cameraId, DeviceOrientation orientation) { + throw UnimplementedError('lockCaptureOrientation() is not implemented.'); + } + + /// Unlocks the capture orientation. + Future unlockCaptureOrientation(int cameraId) { + throw UnimplementedError('unlockCaptureOrientation() is not implemented.'); + } + + /// Captures an image and returns the file where it was saved. + Future takePicture(int cameraId) { + throw UnimplementedError('takePicture() is not implemented.'); + } + + /// Prepare the capture session for video recording. + Future prepareForVideoRecording() { + throw UnimplementedError('prepareForVideoRecording() is not implemented.'); + } + + /// Starts a video recording. + /// + /// The length of the recording can be limited by specifying the [maxVideoDuration]. + /// By default no maximum duration is specified, + /// meaning the recording will continue until manually stopped. + /// The video is returned as a [XFile] after calling [stopVideoRecording]. + Future startVideoRecording(int cameraId, {Duration maxVideoDuration}) { + throw UnimplementedError('startVideoRecording() is not implemented.'); + } + + /// Stops the video recording and returns the file where it was saved. + Future stopVideoRecording(int cameraId) { + throw UnimplementedError('stopVideoRecording() is not implemented.'); + } + + /// Pause video recording. + Future pauseVideoRecording(int cameraId) { + throw UnimplementedError('pauseVideoRecording() is not implemented.'); + } + + /// Resume video recording after pausing. + Future resumeVideoRecording(int cameraId) { + throw UnimplementedError('resumeVideoRecording() is not implemented.'); + } + + /// Sets the flash mode for the selected camera. + Future setFlashMode(int cameraId, FlashMode mode) { + throw UnimplementedError('setFlashMode() is not implemented.'); + } + + /// Sets the exposure mode for taking pictures. + Future setExposureMode(int cameraId, ExposureMode mode) { + throw UnimplementedError('setExposureMode() is not implemented.'); + } + + /// Sets the exposure point for automatically determining the exposure values. + Future setExposurePoint(int cameraId, Point point) { + throw UnimplementedError('setExposurePoint() is not implemented.'); + } + + /// Gets the minimum supported exposure offset for the selected camera in EV units. + Future getMinExposureOffset(int cameraId) { + throw UnimplementedError('getMinExposureOffset() is not implemented.'); + } + + /// Gets the maximum supported exposure offset for the selected camera in EV units. + Future getMaxExposureOffset(int cameraId) { + throw UnimplementedError('getMaxExposureOffset() is not implemented.'); + } + + /// Gets the supported step size for exposure offset for the selected camera in EV units. + /// + /// Returns 0 when the camera supports using a free value without stepping. + Future getExposureOffsetStepSize(int cameraId) { + throw UnimplementedError('getMinExposureOffset() is not implemented.'); + } + + /// Sets the exposure offset for the selected camera. + /// + /// The supplied [offset] value should be in EV units. 1 EV unit represents a + /// doubling in brightness. It should be between the minimum and maximum offsets + /// obtained through `getMinExposureOffset` and `getMaxExposureOffset` respectively. + /// Throws a `CameraException` when an illegal offset is supplied. + /// + /// When the supplied [offset] value does not align with the step size obtained + /// through `getExposureStepSize`, it will automatically be rounded to the nearest step. + /// + /// Returns the (rounded) offset value that was set. + Future setExposureOffset(int cameraId, double offset) { + throw UnimplementedError('setExposureOffset() is not implemented.'); + } + + /// Sets the focus mode for taking pictures. + Future setFocusMode(int cameraId, FocusMode mode) { + throw UnimplementedError('setFocusMode() is not implemented.'); + } + + /// Sets the focus point for automatically determining the focus values. + Future setFocusPoint(int cameraId, Point point) { + throw UnimplementedError('setFocusPoint() is not implemented.'); + } + + /// Gets the maximum supported zoom level for the selected camera. + Future getMaxZoomLevel(int cameraId) { + throw UnimplementedError('getMaxZoomLevel() is not implemented.'); + } + + /// Gets the minimum supported zoom level for the selected camera. + Future getMinZoomLevel(int cameraId) { + throw UnimplementedError('getMinZoomLevel() is not implemented.'); + } + + /// Set the zoom level for the selected camera. + /// + /// The supplied [zoom] value should be between 1.0 and the maximum supported + /// zoom level returned by the `getMaxZoomLevel`. Throws a `CameraException` + /// when an illegal zoom level is supplied. + Future setZoomLevel(int cameraId, double zoom) { + throw UnimplementedError('setZoomLevel() is not implemented.'); + } + + /// Returns a widget showing a live camera preview. + Widget buildPreview(int cameraId) { + throw UnimplementedError('buildView() has not been implemented.'); + } + + /// Releases the resources of this camera. + Future dispose(int cameraId) { + throw UnimplementedError('dispose() is not implemented.'); + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart b/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart new file mode 100644 index 000000000000..c19af1f50d1c --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart @@ -0,0 +1,52 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The direction the camera is facing. +enum CameraLensDirection { + /// Front facing camera (a user looking at the screen is seen by the camera). + front, + + /// Back facing camera (a user looking at the screen is not seen by the camera). + back, + + /// External camera which may not be mounted to the device. + external, +} + +/// Properties of a camera device. +class CameraDescription { + /// Creates a new camera description with the given properties. + CameraDescription({this.name, this.lensDirection, this.sensorOrientation}); + + /// The name of the camera device. + final String name; + + /// The direction the camera is facing. + final CameraLensDirection lensDirection; + + /// Clockwise angle through which the output image needs to be rotated to be upright on the device screen in its native orientation. + /// + /// **Range of valid values:** + /// 0, 90, 180, 270 + /// + /// On Android, also defines the direction of rolling shutter readout, which + /// is from top to bottom in the sensor's coordinate system. + final int sensorOrientation; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CameraDescription && + runtimeType == other.runtimeType && + name == other.name && + lensDirection == other.lensDirection; + + @override + int get hashCode => name.hashCode ^ lensDirection.hashCode; + + @override + String toString() { + return '$runtimeType($name, $lensDirection, $sensorOrientation)'; + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/camera_exception.dart b/packages/camera/camera_platform_interface/lib/src/types/camera_exception.dart new file mode 100644 index 000000000000..3da659f7021d --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_exception.dart @@ -0,0 +1,20 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// This is thrown when the plugin reports an error. +class CameraException implements Exception { + /// Creates a new camera exception with the given error code and description. + CameraException(this.code, this.description); + + /// Error code. + // TODO(bparrishMines): Document possible error codes. + // https://github.com/flutter/flutter/issues/69298 + String code; + + /// Textual description of the error. + String description; + + @override + String toString() => 'CameraException($code, $description)'; +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart new file mode 100644 index 000000000000..7fbfbaf5f4a9 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart @@ -0,0 +1,38 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The possible exposure modes that can be set for a camera. +enum ExposureMode { + /// Automatically determine exposure settings. + auto, + + /// Lock the currently determined exposure settings. + locked, +} + +/// Returns the exposure mode as a String. +String serializeExposureMode(ExposureMode exposureMode) { + if (exposureMode == null) return null; + switch (exposureMode) { + case ExposureMode.locked: + return 'locked'; + case ExposureMode.auto: + return 'auto'; + default: + throw ArgumentError('Unknown ExposureMode value'); + } +} + +/// Returns the exposure mode for a given String. +ExposureMode deserializeExposureMode(String str) { + if (str == null) return null; + switch (str) { + case "locked": + return ExposureMode.locked; + case "auto": + return ExposureMode.auto; + default: + throw ArgumentError('"$str" is not a valid ExposureMode value'); + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/flash_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/flash_mode.dart new file mode 100644 index 000000000000..7feb59caeab8 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/flash_mode.dart @@ -0,0 +1,18 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The possible flash modes that can be set for a camera +enum FlashMode { + /// Do not use the flash when taking a picture. + off, + + /// Let the device decide whether to flash the camera when taking a picture. + auto, + + /// Always use the flash when taking a picture. + always, + + /// Turns on the flash light and keeps it on until switched off. + torch, +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart new file mode 100644 index 000000000000..ad5e9a2d46f1 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart @@ -0,0 +1,38 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The possible focus modes that can be set for a camera. +enum FocusMode { + /// Automatically determine focus settings. + auto, + + /// Lock the currently determined focus settings. + locked, +} + +/// Returns the focus mode as a String. +String serializeFocusMode(FocusMode focusMode) { + if (focusMode == null) return null; + switch (focusMode) { + case FocusMode.locked: + return 'locked'; + case FocusMode.auto: + return 'auto'; + default: + throw ArgumentError('Unknown FocusMode value'); + } +} + +/// Returns the focus mode for a given String. +FocusMode deserializeFocusMode(String str) { + if (str == null) return null; + switch (str) { + case "locked": + return FocusMode.locked; + case "auto": + return FocusMode.auto; + default: + throw ArgumentError('"$str" is not a valid FocusMode value'); + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart new file mode 100644 index 000000000000..3d2c0180fe65 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart @@ -0,0 +1,50 @@ +/// Group of image formats that are comparable across Android and iOS platforms. +enum ImageFormatGroup { + /// The image format does not fit into any specific group. + unknown, + + /// Multi-plane YUV 420 format. + /// + /// This format is a generic YCbCr format, capable of describing any 4:2:0 + /// chroma-subsampled planar or semiplanar buffer (but not fully interleaved), + /// with 8 bits per color sample. + /// + /// On Android, this is `android.graphics.ImageFormat.YUV_420_888`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat.html#YUV_420_888 + /// + /// On iOS, this is `kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`. See + /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_420ypcbcr8biplanarvideorange?language=objc + yuv420, + + /// 32-bit BGRA. + /// + /// On iOS, this is `kCVPixelFormatType_32BGRA`. See + /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_32bgra?language=objc + bgra8888, + + /// 32-big RGB image encoded into JPEG bytes. + /// + /// On Android, this is `android.graphics.ImageFormat.JPEG`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat#JPEG + jpeg, +} + +/// Extension on [ImageFormatGroup] to stringify the enum +extension ImageFormatGroupName on ImageFormatGroup { + /// returns a String value for [ImageFormatGroup] + /// returns 'unknown' if platform is not supported + /// or if [ImageFormatGroup] is not supported for the platform + String name() { + switch (this) { + case ImageFormatGroup.bgra8888: + return 'bgra8888'; + case ImageFormatGroup.yuv420: + return 'yuv420'; + case ImageFormatGroup.jpeg: + return 'jpeg'; + case ImageFormatGroup.unknown: + default: + return 'unknown'; + } + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart b/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart new file mode 100644 index 000000000000..ead592364131 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart @@ -0,0 +1,26 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Affect the quality of video recording and image capture: +/// +/// If a preset is not available on the camera being used a preset of lower quality will be selected automatically. +enum ResolutionPreset { + /// 352x288 on iOS, 240p (320x240) on Android + low, + + /// 480p (640x480 on iOS, 720x480 on Android) + medium, + + /// 720p (1280x720) + high, + + /// 1080p (1920x1080) + veryHigh, + + /// 2160p (3840x2160) + ultraHigh, + + /// The highest resolution available. + max, +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart new file mode 100644 index 000000000000..256558dff3e7 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/types.dart @@ -0,0 +1,11 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'camera_description.dart'; +export 'resolution_preset.dart'; +export 'camera_exception.dart'; +export 'flash_mode.dart'; +export 'image_format_group.dart'; +export 'exposure_mode.dart'; +export 'focus_mode.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart new file mode 100644 index 000000000000..5413f25bb8b7 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart @@ -0,0 +1,47 @@ +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; + +/// Parses a string into a corresponding CameraLensDirection. +CameraLensDirection parseCameraLensDirection(String string) { + switch (string) { + case 'front': + return CameraLensDirection.front; + case 'back': + return CameraLensDirection.back; + case 'external': + return CameraLensDirection.external; + } + throw ArgumentError('Unknown CameraLensDirection value'); +} + +/// Returns the device orientation as a String. +String serializeDeviceOrientation(DeviceOrientation orientation) { + switch (orientation) { + case DeviceOrientation.portraitUp: + return 'portraitUp'; + case DeviceOrientation.portraitDown: + return 'portraitDown'; + case DeviceOrientation.landscapeRight: + return 'landscapeRight'; + case DeviceOrientation.landscapeLeft: + return 'landscapeLeft'; + default: + throw ArgumentError('Unknown DeviceOrientation value'); + } +} + +/// Returns the device orientation for a given String. +DeviceOrientation deserializeDeviceOrientation(String str) { + switch (str) { + case "portraitUp": + return DeviceOrientation.portraitUp; + case "portraitDown": + return DeviceOrientation.portraitDown; + case "landscapeRight": + return DeviceOrientation.landscapeRight; + case "landscapeLeft": + return DeviceOrientation.landscapeLeft; + default: + throw ArgumentError('"$str" is not a valid DeviceOrientation value'); + } +} diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..2a8d7ce9abe1 --- /dev/null +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -0,0 +1,25 @@ +name: camera_platform_interface +description: A common platform interface for the camera plugin. +homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera_platform_interface +# 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: 1.5.0 + +dependencies: + flutter: + sdk: flutter + meta: ^1.0.5 + plugin_platform_interface: ^1.0.1 + cross_file: ^0.1.0 + stream_transform: ^1.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + async: ^2.4.2 + mockito: ^4.1.1 + pedantic: ^1.8.0 + +environment: + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.22.0" diff --git a/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart new file mode 100644 index 000000000000..8baf5da34159 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -0,0 +1,424 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$CameraPlatform', () { + test('$MethodChannelCamera is the default instance', () { + expect(CameraPlatform.instance, isA()); + }); + + test('Cannot be implemented with `implements`', () { + expect(() { + CameraPlatform.instance = ImplementsCameraPlatform(); + }, throwsNoSuchMethodError); + }); + + test('Can be extended', () { + CameraPlatform.instance = ExtendsCameraPlatform(); + }); + + test('Can be mocked with `implements`', () { + final mock = MockCameraPlatform(); + CameraPlatform.instance = mock; + }); + + test( + 'Default implementation of availableCameras() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.availableCameras(), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onCameraInitialized() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onCameraInitialized(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onResolutionChanged() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onCameraResolutionChanged(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onCameraClosing() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onCameraClosing(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onCameraError() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onCameraError(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onDeviceOrientationChanged() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onDeviceOrientationChanged(), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of lockCaptureOrientation() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.lockCaptureOrientation(1, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of unlockCaptureOrientation() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.unlockCaptureOrientation(1), + throwsUnimplementedError, + ); + }); + + test('Default implementation of dispose() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.dispose(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of createCamera() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.createCamera(null, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of initializeCamera() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.initializeCamera(null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of pauseVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.pauseVideoRecording(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of prepareForVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.prepareForVideoRecording(), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of resumeVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.resumeVideoRecording(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setFlashMode() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setFlashMode(1, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setExposureMode() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setExposureMode(1, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setExposurePoint() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setExposurePoint(1, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getMinExposureOffset() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getMinExposureOffset(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getMaxExposureOffset() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getMaxExposureOffset(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getExposureOffsetStepSize() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getExposureOffsetStepSize(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setExposureOffset() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setExposureOffset(1, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setFocusMode() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setFocusMode(1, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setFocusPoint() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setFocusPoint(1, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of startVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.startVideoRecording(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of stopVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.stopVideoRecording(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of takePicture() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.takePicture(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getMaxZoomLevel() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getMaxZoomLevel(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getMinZoomLevel() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getMinZoomLevel(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setZoomLevel() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setZoomLevel(1, 1.0), + throwsUnimplementedError, + ); + }); + }); +} + +class ImplementsCameraPlatform implements CameraPlatform { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class MockCameraPlatform extends Mock + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin + implements + CameraPlatform {} + +class ExtendsCameraPlatform extends CameraPlatform {} diff --git a/packages/camera/camera_platform_interface/test/events/camera_event_test.dart b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart new file mode 100644 index 000000000000..f146b7e7e7bc --- /dev/null +++ b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart @@ -0,0 +1,322 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/types/exposure_mode.dart'; +import 'package:camera_platform_interface/src/types/focus_mode.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraInitializedEvent tests', () { + test('Constructor should initialize all properties', () { + final event = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + + expect(event.cameraId, 1); + expect(event.previewWidth, 1024); + expect(event.previewHeight, 640); + expect(event.exposureMode, ExposureMode.auto); + expect(event.focusMode, FocusMode.auto); + expect(event.exposurePointSupported, true); + expect(event.focusPointSupported, true); + }); + + test('fromJson should initialize all properties', () { + final event = CameraInitializedEvent.fromJson({ + 'cameraId': 1, + 'previewWidth': 1024.0, + 'previewHeight': 640.0, + 'exposureMode': 'auto', + 'exposurePointSupported': true, + 'focusMode': 'auto', + 'focusPointSupported': true + }); + + expect(event.cameraId, 1); + expect(event.previewWidth, 1024); + expect(event.previewHeight, 640); + expect(event.exposureMode, ExposureMode.auto); + expect(event.exposurePointSupported, true); + expect(event.focusMode, FocusMode.auto); + expect(event.focusPointSupported, true); + }); + + test('toJson should return a map with all fields', () { + final event = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 7); + expect(jsonMap['cameraId'], 1); + expect(jsonMap['previewWidth'], 1024); + expect(jsonMap['previewHeight'], 640); + expect(jsonMap['exposureMode'], 'auto'); + expect(jsonMap['exposurePointSupported'], true); + expect(jsonMap['focusMode'], 'auto'); + expect(jsonMap['focusPointSupported'], true); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if cameraId is different', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 2, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if previewWidth is different', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 2048, 640, ExposureMode.auto, true, FocusMode.auto, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if previewHeight is different', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 1024, 980, ExposureMode.auto, true, FocusMode.auto, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if exposureMode is different', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.locked, true, FocusMode.auto, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if exposurePointSupported is different', + () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, false, FocusMode.auto, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if focusMode is different', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.locked, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if focusPointSupported is different', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, false); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final expectedHashCode = event.cameraId.hashCode ^ + event.previewWidth.hashCode ^ + event.previewHeight.hashCode ^ + event.exposureMode.hashCode ^ + event.exposurePointSupported.hashCode ^ + event.focusMode.hashCode ^ + event.focusPointSupported.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); + + group('CameraResolutionChangesEvent tests', () { + test('Constructor should initialize all properties', () { + final event = CameraResolutionChangedEvent(1, 1024, 640); + + expect(event.cameraId, 1); + expect(event.captureWidth, 1024); + expect(event.captureHeight, 640); + }); + + test('fromJson should initialize all properties', () { + final event = CameraResolutionChangedEvent.fromJson({ + 'cameraId': 1, + 'captureWidth': 1024.0, + 'captureHeight': 640.0, + }); + + expect(event.cameraId, 1); + expect(event.captureWidth, 1024); + expect(event.captureHeight, 640); + }); + + test('toJson should return a map with all fields', () { + final event = CameraResolutionChangedEvent(1, 1024, 640); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 3); + expect(jsonMap['cameraId'], 1); + expect(jsonMap['captureWidth'], 1024); + expect(jsonMap['captureHeight'], 640); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); + final secondEvent = CameraResolutionChangedEvent(1, 1024, 640); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if cameraId is different', () { + final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); + final secondEvent = CameraResolutionChangedEvent(2, 1024, 640); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if captureWidth is different', () { + final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); + final secondEvent = CameraResolutionChangedEvent(1, 2048, 640); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if captureHeight is different', () { + final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); + final secondEvent = CameraResolutionChangedEvent(1, 1024, 980); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = CameraResolutionChangedEvent(1, 1024, 640); + final expectedHashCode = event.cameraId.hashCode ^ + event.captureWidth.hashCode ^ + event.captureHeight.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); + + group('CameraClosingEvent tests', () { + test('Constructor should initialize all properties', () { + final event = CameraClosingEvent(1); + + expect(event.cameraId, 1); + }); + + test('fromJson should initialize all properties', () { + final event = CameraClosingEvent.fromJson({ + 'cameraId': 1, + }); + + expect(event.cameraId, 1); + }); + + test('toJson should return a map with all fields', () { + final event = CameraClosingEvent(1); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 1); + expect(jsonMap['cameraId'], 1); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = CameraClosingEvent(1); + final secondEvent = CameraClosingEvent(1); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if cameraId is different', () { + final firstEvent = CameraClosingEvent(1); + final secondEvent = CameraClosingEvent(2); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = CameraClosingEvent(1); + final expectedHashCode = event.cameraId.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); + + group('CameraErrorEvent tests', () { + test('Constructor should initialize all properties', () { + final event = CameraErrorEvent(1, 'Error'); + + expect(event.cameraId, 1); + expect(event.description, 'Error'); + }); + + test('fromJson should initialize all properties', () { + final event = CameraErrorEvent.fromJson( + {'cameraId': 1, 'description': 'Error'}); + + expect(event.cameraId, 1); + expect(event.description, 'Error'); + }); + + test('toJson should return a map with all fields', () { + final event = CameraErrorEvent(1, 'Error'); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 2); + expect(jsonMap['cameraId'], 1); + expect(jsonMap['description'], 'Error'); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = CameraErrorEvent(1, 'Error'); + final secondEvent = CameraErrorEvent(1, 'Error'); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if cameraId is different', () { + final firstEvent = CameraErrorEvent(1, 'Error'); + final secondEvent = CameraErrorEvent(2, 'Error'); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if description is different', () { + final firstEvent = CameraErrorEvent(1, 'Error'); + final secondEvent = CameraErrorEvent(1, 'Ooops'); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = CameraErrorEvent(1, 'Error'); + final expectedHashCode = + event.cameraId.hashCode ^ event.description.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/events/device_event_test.dart b/packages/camera/camera_platform_interface/test/events/device_event_test.dart new file mode 100644 index 000000000000..c2fef49be04f --- /dev/null +++ b/packages/camera/camera_platform_interface/test/events/device_event_test.dart @@ -0,0 +1,61 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('DeviceOrientationChangedEvent tests', () { + test('Constructor should initialize all properties', () { + final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + + expect(event.orientation, DeviceOrientation.portraitUp); + }); + + test('fromJson should initialize all properties', () { + final event = DeviceOrientationChangedEvent.fromJson({ + 'orientation': 'portraitUp', + }); + + expect(event.orientation, DeviceOrientation.portraitUp); + }); + + test('toJson should return a map with all fields', () { + final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 1); + expect(jsonMap['orientation'], 'portraitUp'); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + final secondEvent = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if orientation is different', () { + final firstEvent = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + final secondEvent = + DeviceOrientationChangedEvent(DeviceOrientation.landscapeLeft); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + final expectedHashCode = event.orientation.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart new file mode 100644 index 000000000000..7e9146344206 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -0,0 +1,900 @@ +// Copyright 2019 The Chromium 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 'dart:math'; + +import 'package:async/async.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/events/device_event.dart'; +import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; +import 'package:camera_platform_interface/src/types/focus_mode.dart'; +import 'package:camera_platform_interface/src/utils/utils.dart'; +import 'package:flutter/services.dart' hide DeviceOrientation; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../utils/method_channel_mock.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$MethodChannelCamera', () { + group('Creation, Initialization & Disposal Tests', () { + test('Should send creation data and receive back a camera id', () async { + // Arrange + MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + } + }); + final camera = MethodChannelCamera(); + + // Act + final cameraId = await camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ); + + // Assert + expect(cameraMockChannel.log, [ + isMethodCall( + 'create', + arguments: { + 'cameraName': 'Test', + 'resolutionPreset': 'high', + 'enableAudio': null + }, + ), + ]); + expect(cameraId, 1); + }); + + test( + 'Should throw CameraException when create throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: 'plugins.flutter.io/camera', methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final camera = MethodChannelCamera(); + + // Act + expect( + () => camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ), + throwsA( + isA() + .having((e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test( + 'Should throw CameraException when create throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: 'plugins.flutter.io/camera', methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final camera = MethodChannelCamera(); + + // Act + expect( + () => camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ), + throwsA( + isA() + .having((e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test('Should send initialization data', () async { + // Arrange + MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + }, + 'initialize': null + }); + final camera = MethodChannelCamera(); + final cameraId = await camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ); + + // Act + Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + )); + await initializeFuture; + + // Assert + expect(cameraId, 1); + expect(cameraMockChannel.log, [ + anything, + isMethodCall( + 'initialize', + arguments: { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + }, + ), + ]); + }); + + test('Should send a disposal call on dispose', () async { + // Arrange + MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': {'cameraId': 1}, + 'initialize': null, + 'dispose': {'cameraId': 1} + }); + + final camera = MethodChannelCamera(); + final cameraId = await camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ); + Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + )); + await initializeFuture; + + // Act + await camera.dispose(cameraId); + + // Assert + expect(cameraId, 1); + expect(cameraMockChannel.log, [ + anything, + anything, + isMethodCall( + 'dispose', + arguments: {'cameraId': 1}, + ), + ]); + }); + }); + + group('Event Tests', () { + MethodChannelCamera camera; + int cameraId; + setUp(() async { + MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': {'cameraId': 1}, + 'initialize': null + }, + ); + camera = MethodChannelCamera(); + cameraId = await camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ); + Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + )); + await initializeFuture; + }); + + test('Should receive initialized event', () async { + // Act + final Stream eventStream = + camera.onCameraInitialized(cameraId); + final streamQueue = StreamQueue(eventStream); + + // Emit test events + final event = CameraInitializedEvent( + cameraId, + 3840, + 2160, + ExposureMode.auto, + true, + FocusMode.auto, + true, + ); + await camera.handleCameraMethodCall( + MethodCall('initialized', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive resolution changes', () async { + // Act + final Stream resolutionStream = + camera.onCameraResolutionChanged(cameraId); + final streamQueue = StreamQueue(resolutionStream); + + // Emit test events + final fhdEvent = CameraResolutionChangedEvent(cameraId, 1920, 1080); + final uhdEvent = CameraResolutionChangedEvent(cameraId, 3840, 2160); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, fhdEvent); + expect(await streamQueue.next, uhdEvent); + expect(await streamQueue.next, fhdEvent); + expect(await streamQueue.next, uhdEvent); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive camera closing events', () async { + // Act + final Stream eventStream = + camera.onCameraClosing(cameraId); + final streamQueue = StreamQueue(eventStream); + + // Emit test events + final event = CameraClosingEvent(cameraId); + await camera.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive camera error events', () async { + // Act + final errorStream = camera.onCameraError(cameraId); + final streamQueue = StreamQueue(errorStream); + + // Emit test events + final event = CameraErrorEvent(cameraId, 'Error Description'); + await camera.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive device orientation change events', () async { + // Act + final eventStream = camera.onDeviceOrientationChanged(); + final streamQueue = StreamQueue(eventStream); + + // Emit test events + final event = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + await camera.handleDeviceMethodCall( + MethodCall('orientation_changed', event.toJson())); + await camera.handleDeviceMethodCall( + MethodCall('orientation_changed', event.toJson())); + await camera.handleDeviceMethodCall( + MethodCall('orientation_changed', event.toJson())); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + }); + + group('Function Tests', () { + MethodChannelCamera camera; + int cameraId; + setUp(() async { + MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': {'cameraId': 1}, + 'initialize': null + }, + ); + camera = MethodChannelCamera(); + cameraId = await camera.createCamera( + CameraDescription(name: 'Test'), + ResolutionPreset.high, + ); + Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add( + CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + ), + ); + await initializeFuture; + }); + + test('Should fetch CameraDescription instances for available cameras', + () async { + // Arrange + List> returnData = [ + {'name': 'Test 1', 'lensFacing': 'front', 'sensorOrientation': 1}, + {'name': 'Test 2', 'lensFacing': 'back', 'sensorOrientation': 2} + ]; + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'availableCameras': returnData}, + ); + + // Act + List cameras = await camera.availableCameras(); + + // Assert + expect(channel.log, [ + isMethodCall('availableCameras', arguments: null), + ]); + expect(cameras.length, returnData.length); + for (int i = 0; i < returnData.length; i++) { + CameraDescription cameraDescription = CameraDescription( + name: returnData[i]['name'], + lensDirection: + parseCameraLensDirection(returnData[i]['lensFacing']), + sensorOrientation: returnData[i]['sensorOrientation'], + ); + expect(cameras[i], cameraDescription); + } + }); + + test( + 'Should throw CameraException when availableCameras throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: 'plugins.flutter.io/camera', methods: { + 'availableCameras': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + + // Act + expect( + camera.availableCameras, + throwsA( + isA() + .having((e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test('Should take a picture and return an XFile instance', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'takePicture': '/test/path.jpg'}); + + // Act + XFile file = await camera.takePicture(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('takePicture', arguments: { + 'cameraId': cameraId, + }), + ]); + expect(file.path, '/test/path.jpg'); + }); + + test('Should prepare for video recording', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'prepareForVideoRecording': null}, + ); + + // Act + await camera.prepareForVideoRecording(); + + // Assert + expect(channel.log, [ + isMethodCall('prepareForVideoRecording', arguments: null), + ]); + }); + + test('Should start recording a video', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', arguments: { + 'cameraId': cameraId, + 'maxVideoDuration': null, + }), + ]); + }); + + test('Should pass maxVideoDuration when starting recording a video', + () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoRecording( + cameraId, + maxVideoDuration: Duration(seconds: 10), + ); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', + arguments: {'cameraId': cameraId, 'maxVideoDuration': 10000}), + ]); + }); + + test('Should stop a video recording and return the file', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'stopVideoRecording': '/test/path.mp4'}, + ); + + // Act + XFile file = await camera.stopVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('stopVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + expect(file.path, '/test/path.mp4'); + }); + + test('Should pause a video recording', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'pauseVideoRecording': null}, + ); + + // Act + await camera.pauseVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('pauseVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should resume a video recording', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'resumeVideoRecording': null}, + ); + + // Act + await camera.resumeVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('resumeVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the flash mode', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setFlashMode': null}, + ); + + // Act + await camera.setFlashMode(cameraId, FlashMode.torch); + await camera.setFlashMode(cameraId, FlashMode.always); + await camera.setFlashMode(cameraId, FlashMode.auto); + await camera.setFlashMode(cameraId, FlashMode.off); + + // Assert + expect(channel.log, [ + isMethodCall('setFlashMode', + arguments: {'cameraId': cameraId, 'mode': 'torch'}), + isMethodCall('setFlashMode', + arguments: {'cameraId': cameraId, 'mode': 'always'}), + isMethodCall('setFlashMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setFlashMode', + arguments: {'cameraId': cameraId, 'mode': 'off'}), + ]); + }); + + test('Should set the exposure mode', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setExposureMode': null}, + ); + + // Act + await camera.setExposureMode(cameraId, ExposureMode.auto); + await camera.setExposureMode(cameraId, ExposureMode.locked); + + // Assert + expect(channel.log, [ + isMethodCall('setExposureMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setExposureMode', + arguments: {'cameraId': cameraId, 'mode': 'locked'}), + ]); + }); + + test('Should set the exposure point', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setExposurePoint': null}, + ); + + // Act + await camera.setExposurePoint(cameraId, Point(0.5, 0.5)); + await camera.setExposurePoint(cameraId, null); + + // Assert + expect(channel.log, [ + isMethodCall('setExposurePoint', arguments: { + 'cameraId': cameraId, + 'x': 0.5, + 'y': 0.5, + 'reset': false + }), + isMethodCall('setExposurePoint', arguments: { + 'cameraId': cameraId, + 'x': null, + 'y': null, + 'reset': true + }), + ]); + }); + + test('Should get the min exposure offset', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getMinExposureOffset': 2.0}, + ); + + // Act + final minExposureOffset = await camera.getMinExposureOffset(cameraId); + + // Assert + expect(minExposureOffset, 2.0); + expect(channel.log, [ + isMethodCall('getMinExposureOffset', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the max exposure offset', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getMaxExposureOffset': 2.0}, + ); + + // Act + final maxExposureOffset = await camera.getMaxExposureOffset(cameraId); + + // Assert + expect(maxExposureOffset, 2.0); + expect(channel.log, [ + isMethodCall('getMaxExposureOffset', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the exposure offset step size', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getExposureOffsetStepSize': 0.25}, + ); + + // Act + final stepSize = await camera.getExposureOffsetStepSize(cameraId); + + // Assert + expect(stepSize, 0.25); + expect(channel.log, [ + isMethodCall('getExposureOffsetStepSize', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the exposure offset', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setExposureOffset': 0.6}, + ); + + // Act + final actualOffset = await camera.setExposureOffset(cameraId, 0.5); + + // Assert + expect(actualOffset, 0.6); + expect(channel.log, [ + isMethodCall('setExposureOffset', arguments: { + 'cameraId': cameraId, + 'offset': 0.5, + }), + ]); + }); + + test('Should set the focus mode', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setFocusMode': null}, + ); + + // Act + await camera.setFocusMode(cameraId, FocusMode.auto); + await camera.setFocusMode(cameraId, FocusMode.locked); + + // Assert + expect(channel.log, [ + isMethodCall('setFocusMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setFocusMode', + arguments: {'cameraId': cameraId, 'mode': 'locked'}), + ]); + }); + + test('Should set the exposure point', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setFocusPoint': null}, + ); + + // Act + await camera.setFocusPoint(cameraId, Point(0.5, 0.5)); + await camera.setFocusPoint(cameraId, null); + + // Assert + expect(channel.log, [ + isMethodCall('setFocusPoint', arguments: { + 'cameraId': cameraId, + 'x': 0.5, + 'y': 0.5, + 'reset': false + }), + isMethodCall('setFocusPoint', arguments: { + 'cameraId': cameraId, + 'x': null, + 'y': null, + 'reset': true + }), + ]); + }); + + test('Should build a texture widget as preview widget', () async { + // Act + Widget widget = camera.buildPreview(cameraId); + + // Act + expect(widget is Texture, isTrue); + expect((widget as Texture).textureId, cameraId); + }); + + test('Should throw MissingPluginException when handling unknown method', + () { + final camera = MethodChannelCamera(); + + expect( + () => + camera.handleCameraMethodCall(MethodCall('unknown_method'), 1), + throwsA(isA())); + }); + + test('Should get the max zoom level', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getMaxZoomLevel': 10.0}, + ); + + // Act + final maxZoomLevel = await camera.getMaxZoomLevel(cameraId); + + // Assert + expect(maxZoomLevel, 10.0); + expect(channel.log, [ + isMethodCall('getMaxZoomLevel', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the min zoom level', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getMinZoomLevel': 1.0}, + ); + + // Act + final maxZoomLevel = await camera.getMinZoomLevel(cameraId); + + // Assert + expect(maxZoomLevel, 1.0); + expect(channel.log, [ + isMethodCall('getMinZoomLevel', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the zoom level', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setZoomLevel': null}, + ); + + // Act + await camera.setZoomLevel(cameraId, 2.0); + + // Assert + expect(channel.log, [ + isMethodCall('setZoomLevel', + arguments: {'cameraId': cameraId, 'zoom': 2.0}), + ]); + }); + + test('Should throw CameraException when illegal zoom level is supplied', + () async { + // Arrange + MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'setZoomLevel': PlatformException( + code: 'ZOOM_ERROR', + message: 'Illegal zoom error', + details: null, + ) + }, + ); + + // Act & assert + expect( + () => camera.setZoomLevel(cameraId, -1.0), + throwsA(isA() + .having((e) => e.code, 'code', 'ZOOM_ERROR') + .having((e) => e.description, 'description', + 'Illegal zoom error'))); + }); + + test('Should lock the capture orientation', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'lockCaptureOrientation': null}, + ); + + // Act + await camera.lockCaptureOrientation( + cameraId, DeviceOrientation.portraitUp); + + // Assert + expect(channel.log, [ + isMethodCall('lockCaptureOrientation', + arguments: {'cameraId': cameraId, 'orientation': 'portraitUp'}), + ]); + }); + + test('Should unlock the capture orientation', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'unlockCaptureOrientation': null}, + ); + + // Act + await camera.unlockCaptureOrientation(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('unlockCaptureOrientation', + arguments: {'cameraId': cameraId}), + ]); + }); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/camera_description_test.dart b/packages/camera/camera_platform_interface/test/types/camera_description_test.dart new file mode 100644 index 000000000000..03909dbafb97 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/camera_description_test.dart @@ -0,0 +1,113 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraLensDirection tests', () { + test('CameraLensDirection should contain 3 options', () { + final values = CameraLensDirection.values; + + expect(values.length, 3); + }); + + test("CameraLensDirection enum should have items in correct index", () { + final values = CameraLensDirection.values; + + expect(values[0], CameraLensDirection.front); + expect(values[1], CameraLensDirection.back); + expect(values[2], CameraLensDirection.external); + }); + }); + + group('CameraDescription tests', () { + test('Constructor should initialize all properties', () { + final description = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + + expect(description.name, 'Test'); + expect(description.lensDirection, CameraLensDirection.front); + expect(description.sensorOrientation, 90); + }); + + test('equals should return true if objects are the same', () { + final firstDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + final secondDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + + expect(firstDescription == secondDescription, true); + }); + + test('equals should return false if name is different', () { + final firstDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + final secondDescription = CameraDescription( + name: 'Testing', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + + expect(firstDescription == secondDescription, false); + }); + + test('equals should return false if lens direction is different', () { + final firstDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + final secondDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ); + + expect(firstDescription == secondDescription, false); + }); + + test('equals should return true if sensor orientation is different', () { + final firstDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ); + final secondDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + + expect(firstDescription == secondDescription, true); + }); + + test('hashCode should match hashCode of all properties', () { + final description = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ); + final expectedHashCode = description.name.hashCode ^ + description.lensDirection.hashCode ^ + description.sensorOrientation.hashCode; + + expect(description.hashCode, expectedHashCode); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/camera_exception_test.dart b/packages/camera/camera_platform_interface/test/types/camera_exception_test.dart new file mode 100644 index 000000000000..17370e4561f5 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/camera_exception_test.dart @@ -0,0 +1,28 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('constructor should initialize properties', () { + final code = 'TEST_ERROR'; + final description = 'This is a test error'; + final exception = CameraException(code, description); + + expect(exception.code, code); + expect(exception.description, description); + }); + + test('toString: Should return a description of the exception', () { + final code = 'TEST_ERROR'; + final description = 'This is a test error'; + final expected = 'CameraException($code, $description)'; + final exception = CameraException(code, description); + + final actual = exception.toString(); + + expect(actual, expected); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart b/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart new file mode 100644 index 000000000000..c34c1d7b4157 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart @@ -0,0 +1,32 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/types/exposure_mode.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('ExposureMode should contain 2 options', () { + final values = ExposureMode.values; + + expect(values.length, 2); + }); + + test("ExposureMode enum should have items in correct index", () { + final values = ExposureMode.values; + + expect(values[0], ExposureMode.auto); + expect(values[1], ExposureMode.locked); + }); + + test("serializeExposureMode() should serialize correctly", () { + expect(serializeExposureMode(ExposureMode.auto), "auto"); + expect(serializeExposureMode(ExposureMode.locked), "locked"); + }); + + test("deserializeExposureMode() should deserialize correctly", () { + expect(deserializeExposureMode('auto'), ExposureMode.auto); + expect(deserializeExposureMode('locked'), ExposureMode.locked); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/flash_mode_test.dart b/packages/camera/camera_platform_interface/test/types/flash_mode_test.dart new file mode 100644 index 000000000000..c9df64152cef --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/flash_mode_test.dart @@ -0,0 +1,23 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('FlashMode should contain 4 options', () { + final values = FlashMode.values; + + expect(values.length, 4); + }); + + test("FlashMode enum should have items in correct index", () { + final values = FlashMode.values; + + expect(values[0], FlashMode.off); + expect(values[1], FlashMode.auto); + expect(values[2], FlashMode.always); + expect(values[3], FlashMode.torch); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/focus_mode_test.dart b/packages/camera/camera_platform_interface/test/types/focus_mode_test.dart new file mode 100644 index 000000000000..ca7ad902820a --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/focus_mode_test.dart @@ -0,0 +1,31 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/src/types/focus_mode.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('FocusMode should contain 2 options', () { + final values = FocusMode.values; + + expect(values.length, 2); + }); + + test("FocusMode enum should have items in correct index", () { + final values = FocusMode.values; + + expect(values[0], FocusMode.auto); + expect(values[1], FocusMode.locked); + }); + + test("serializeFocusMode() should serialize correctly", () { + expect(serializeFocusMode(FocusMode.auto), "auto"); + expect(serializeFocusMode(FocusMode.locked), "locked"); + }); + + test("deserializeFocusMode() should deserialize correctly", () { + expect(deserializeFocusMode('auto'), FocusMode.auto); + expect(deserializeFocusMode('locked'), FocusMode.locked); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/image_group_test.dart b/packages/camera/camera_platform_interface/test/types/image_group_test.dart new file mode 100644 index 000000000000..c49b2f03a7a0 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/image_group_test.dart @@ -0,0 +1,13 @@ +import 'package:camera_platform_interface/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('$ImageFormatGroup tests', () { + test('ImageFormatGroupName extension returns correct values', () { + expect(ImageFormatGroup.bgra8888.name(), 'bgra8888'); + expect(ImageFormatGroup.yuv420.name(), 'yuv420'); + expect(ImageFormatGroup.jpeg.name(), 'jpeg'); + expect(ImageFormatGroup.unknown.name(), 'unknown'); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart b/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart new file mode 100644 index 000000000000..aadf589f87c4 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart @@ -0,0 +1,25 @@ +// Copyright 2019 The Chromium 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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('ResolutionPreset should contain 6 options', () { + final values = ResolutionPreset.values; + + expect(values.length, 6); + }); + + test("ResolutionPreset enum should have items in correct index", () { + final values = ResolutionPreset.values; + + expect(values[0], ResolutionPreset.low); + expect(values[1], ResolutionPreset.medium); + expect(values[2], ResolutionPreset.high); + expect(values[3], ResolutionPreset.veryHigh); + expect(values[4], ResolutionPreset.ultraHigh); + expect(values[5], ResolutionPreset.max); + }); +} diff --git a/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart b/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart new file mode 100644 index 000000000000..cdf393f82b5f --- /dev/null +++ b/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart @@ -0,0 +1,38 @@ +// Copyright 2019 The Chromium 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/services.dart'; + +class MethodChannelMock { + final Duration delay; + final MethodChannel methodChannel; + final Map methods; + final log = []; + + MethodChannelMock({ + String channelName, + this.delay, + this.methods, + }) : methodChannel = MethodChannel(channelName) { + methodChannel.setMockMethodCallHandler(_handler); + } + + Future _handler(MethodCall methodCall) async { + log.add(methodCall); + + if (!methods.containsKey(methodCall.method)) { + throw MissingPluginException('No implementation found for method ' + '${methodCall.method} on channel ${methodChannel.name}'); + } + + return Future.delayed(delay ?? Duration.zero, () { + final result = methods[methodCall.method]; + if (result is Exception) { + throw result; + } + + return Future.value(result); + }); + } +} diff --git a/packages/camera/camera_platform_interface/test/utils/utils_test.dart b/packages/camera/camera_platform_interface/test/utils/utils_test.dart new file mode 100644 index 000000000000..63e3baff265d --- /dev/null +++ b/packages/camera/camera_platform_interface/test/utils/utils_test.dart @@ -0,0 +1,56 @@ +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/utils/utils.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Utility methods', () { + test( + 'Should return CameraLensDirection when valid value is supplied when parsing camera lens direction', + () { + expect( + parseCameraLensDirection('back'), + CameraLensDirection.back, + ); + expect( + parseCameraLensDirection('front'), + CameraLensDirection.front, + ); + expect( + parseCameraLensDirection('external'), + CameraLensDirection.external, + ); + }); + + test( + 'Should throw ArgumentException when invalid value is supplied when parsing camera lens direction', + () { + expect( + () => parseCameraLensDirection('test'), + throwsA(isArgumentError), + ); + }); + + test("serializeDeviceOrientation() should serialize correctly", () { + expect(serializeDeviceOrientation(DeviceOrientation.portraitUp), + "portraitUp"); + expect(serializeDeviceOrientation(DeviceOrientation.portraitDown), + "portraitDown"); + expect(serializeDeviceOrientation(DeviceOrientation.landscapeRight), + "landscapeRight"); + expect(serializeDeviceOrientation(DeviceOrientation.landscapeLeft), + "landscapeLeft"); + }); + + test("deserializeDeviceOrientation() should deserialize correctly", () { + expect(deserializeDeviceOrientation('portraitUp'), + DeviceOrientation.portraitUp); + expect(deserializeDeviceOrientation('portraitDown'), + DeviceOrientation.portraitDown); + expect(deserializeDeviceOrientation('landscapeRight'), + DeviceOrientation.landscapeRight); + expect(deserializeDeviceOrientation('landscapeLeft'), + DeviceOrientation.landscapeLeft); + }); + }); +} diff --git a/packages/camera/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..be52383ef49c --- /dev/null +++ b/packages/camera/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/packages/connectivity/connectivity/CHANGELOG.md b/packages/connectivity/connectivity/CHANGELOG.md index 72b6d9b6513a..a1d0231a5bd4 100644 --- a/packages/connectivity/connectivity/CHANGELOG.md +++ b/packages/connectivity/connectivity/CHANGELOG.md @@ -1,3 +1,23 @@ +## 3.0.0-nullsafety.3 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 3.0.0-nullsafety.2 + +* Android: Cleanup the NetworkCallback object when a connectivity stream is cancelled + +## 3.0.0-nullsafety.1 + +* Bump Dart SDK to support null safety. + +## 3.0.0-nullsafety + +* Migrate to null safety. + +## 2.0.3 + +* Update Flutter SDK constraint. + ## 2.0.2 * Android: Fix IllegalArgumentException. diff --git a/packages/connectivity/connectivity/README.md b/packages/connectivity/connectivity/README.md index a6a1dcc2838a..6c608d3d8e16 100644 --- a/packages/connectivity/connectivity/README.md +++ b/packages/connectivity/connectivity/README.md @@ -55,6 +55,6 @@ Note that connectivity changes are no longer communicated to Android apps in the ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java b/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java index ca3ccff82d2d..dbf96bda9fe8 100644 --- a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java +++ b/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java @@ -64,6 +64,7 @@ public void onCancel(Object arguments) { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (networkCallback != null) { connectivity.getConnectivityManager().unregisterNetworkCallback(networkCallback); + networkCallback = null; } } else { context.unregisterReceiver(this); diff --git a/packages/connectivity/connectivity/example/lib/main.dart b/packages/connectivity/connectivity/example/lib/main.dart index e05497136b86..19285ce889fd 100644 --- a/packages/connectivity/connectivity/example/lib/main.dart +++ b/packages/connectivity/connectivity/example/lib/main.dart @@ -40,7 +40,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @@ -51,7 +51,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { String _connectionStatus = 'Unknown'; final Connectivity _connectivity = Connectivity(); - StreamSubscription _connectivitySubscription; + late StreamSubscription _connectivitySubscription; @override void initState() { @@ -69,7 +69,7 @@ class _MyHomePageState extends State { // Platform messages are asynchronous, so we initialize in an async method. Future initConnectivity() async { - ConnectivityResult result; + ConnectivityResult result = ConnectivityResult.none; // Platform messages may fail, so we use a try/catch PlatformException. try { result = await _connectivity.checkConnectivity(); diff --git a/packages/connectivity/connectivity/example/pubspec.yaml b/packages/connectivity/connectivity/example/pubspec.yaml index bff35483d763..682f0a2dd08a 100644 --- a/packages/connectivity/connectivity/example/pubspec.yaml +++ b/packages/connectivity/connectivity/example/pubspec.yaml @@ -10,13 +10,14 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter + test: ^1.10.0-nullsafety.1 integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0-0 <3.0.0" flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/packages/connectivity/connectivity/example/test_driver/integration_test/connectivity_test.dart b/packages/connectivity/connectivity/example/test_driver/integration_test/connectivity_test.dart index d48deae3403d..3177b66ea48a 100644 --- a/packages/connectivity/connectivity/example/test_driver/integration_test/connectivity_test.dart +++ b/packages/connectivity/connectivity/example/test_driver/integration_test/connectivity_test.dart @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(cyanglaz): Remove once https://github.com/flutter/plugins/pull/3158 is landed. +// @dart = 2.9 + import 'package:integration_test/integration_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:connectivity/connectivity.dart'; diff --git a/packages/connectivity/connectivity/example/test_driver/test/integration_test.dart b/packages/connectivity/connectivity/example/test_driver/test/integration_test.dart index c0cbdcab2fb6..33421c335bdd 100644 --- a/packages/connectivity/connectivity/example/test_driver/test/integration_test.dart +++ b/packages/connectivity/connectivity/example/test_driver/test/integration_test.dart @@ -2,6 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(amirh): Remove this once flutter_driver supports null safety. +// https://github.com/flutter/flutter/issues/71379 +// @dart = 2.9 + import 'dart:convert'; import 'dart:io'; import 'package:flutter_driver/flutter_driver.dart'; diff --git a/packages/connectivity/connectivity/integration_test/connectivity_test.dart b/packages/connectivity/connectivity/integration_test/connectivity_test.dart index d48deae3403d..3177b66ea48a 100644 --- a/packages/connectivity/connectivity/integration_test/connectivity_test.dart +++ b/packages/connectivity/connectivity/integration_test/connectivity_test.dart @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(cyanglaz): Remove once https://github.com/flutter/plugins/pull/3158 is landed. +// @dart = 2.9 + import 'package:integration_test/integration_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:connectivity/connectivity.dart'; diff --git a/packages/connectivity/connectivity/lib/connectivity.dart b/packages/connectivity/connectivity/lib/connectivity.dart index c9655365f772..0f30a9347b74 100644 --- a/packages/connectivity/connectivity/lib/connectivity.dart +++ b/packages/connectivity/connectivity/lib/connectivity.dart @@ -22,12 +22,12 @@ class Connectivity { if (_singleton == null) { _singleton = Connectivity._(); } - return _singleton; + return _singleton!; } Connectivity._(); - static Connectivity _singleton; + static Connectivity? _singleton; static ConnectivityPlatform get _platform => ConnectivityPlatform.instance; diff --git a/packages/connectivity/connectivity/pubspec.yaml b/packages/connectivity/connectivity/pubspec.yaml index 91d06373ab83..7ae03553a26c 100644 --- a/packages/connectivity/connectivity/pubspec.yaml +++ b/packages/connectivity/connectivity/pubspec.yaml @@ -2,7 +2,7 @@ name: connectivity description: Flutter plugin for discovering the state of the network (WiFi & mobile/cellular) connectivity on Android and iOS. homepage: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity -version: 2.0.2 +version: 3.0.0-nullsafety.3 flutter: plugin: @@ -21,21 +21,24 @@ dependencies: flutter: sdk: flutter meta: ^1.0.5 - connectivity_platform_interface: ^1.0.2 - connectivity_macos: ^0.1.0 - connectivity_for_web: ^0.3.0 + connectivity_platform_interface: ^2.0.0-nullsafety.1 + #TODO(cyanglaz): re-endorse the below plugins when they have migrated to nnbd. + # https://github.com/flutter/flutter/issues/68669 + connectivity_macos: ^0.2.0-nullsafety + # connectivity_for_web: ^0.3.0 dev_dependencies: flutter_test: sdk: flutter flutter_driver: sdk: flutter + test: ^1.10.0-nullsafety.1 integration_test: path: ../../integration_test mockito: ^4.1.1 - plugin_platform_interface: ^1.0.0 - pedantic: ^1.8.0 + plugin_platform_interface: ^1.1.0-nullsafety.1 + pedantic: ^1.10.0-nullsafety.1 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/connectivity/connectivity/test/connectivity_test.dart b/packages/connectivity/connectivity/test/connectivity_test.dart index b7749ca7a6be..e83196546cd2 100644 --- a/packages/connectivity/connectivity/test/connectivity_test.dart +++ b/packages/connectivity/connectivity/test/connectivity_test.dart @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 - +// TODO(cyanglaz): Remove once Mockito is migrated to null safety. +// @dart = 2.9 import 'package:connectivity/connectivity.dart'; import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/connectivity/connectivity_macos/CHANGELOG.md b/packages/connectivity/connectivity_macos/CHANGELOG.md index 87910374bb8b..9261b0e789fe 100644 --- a/packages/connectivity/connectivity_macos/CHANGELOG.md +++ b/packages/connectivity/connectivity_macos/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.2.0-nullsafety + +* Update Dart SDK constraint. + +## 0.1.0+8 + +* Update Flutter SDK constraint. + ## 0.1.0+7 * Remove unused `test` dependency. diff --git a/packages/connectivity/connectivity_macos/pubspec.yaml b/packages/connectivity/connectivity_macos/pubspec.yaml index 2ab493072f8c..dd193f715c2a 100644 --- a/packages/connectivity/connectivity_macos/pubspec.yaml +++ b/packages/connectivity/connectivity_macos/pubspec.yaml @@ -3,7 +3,7 @@ description: macOS implementation of the connectivity plugin. # 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.1.0+7 +version: 0.2.0-nullsafety homepage: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_macos flutter: @@ -13,8 +13,8 @@ flutter: pluginClass: ConnectivityPlugin environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.10.0" dependencies: flutter: diff --git a/packages/connectivity/connectivity_platform_interface/CHANGELOG.md b/packages/connectivity/connectivity_platform_interface/CHANGELOG.md index e45f8f7e4d99..8e38341be42f 100644 --- a/packages/connectivity/connectivity_platform_interface/CHANGELOG.md +++ b/packages/connectivity/connectivity_platform_interface/CHANGELOG.md @@ -1,3 +1,15 @@ +## 2.0.0-nullsafety.1 + +* Bump Dart SDK to support null safety. + +## 2.0.0-nullsafety + +* Migrate to null safety. + +## 1.0.7 + +* Update Flutter SDK constraint. + ## 1.0.6 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart b/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart index cfd9cf648a9c..8e9f0fd6af06 100644 --- a/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart +++ b/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart @@ -50,17 +50,17 @@ abstract class ConnectivityPlatform extends PlatformInterface { } /// Obtains the wifi name (SSID) of the connected network - Future getWifiName() { + Future getWifiName() { throw UnimplementedError('getWifiName() has not been implemented.'); } /// Obtains the wifi BSSID of the connected network. - Future getWifiBSSID() { + Future getWifiBSSID() { throw UnimplementedError('getWifiBSSID() has not been implemented.'); } /// Obtains the IP address of the connected wifi network - Future getWifiIP() { + Future getWifiIP() { throw UnimplementedError('getWifiIP() has not been implemented.'); } diff --git a/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart b/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart index 87deaa21ea3b..b411b5bc069b 100644 --- a/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart +++ b/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart @@ -22,29 +22,29 @@ class MethodChannelConnectivity extends ConnectivityPlatform { EventChannel eventChannel = EventChannel('plugins.flutter.io/connectivity_status'); - Stream _onConnectivityChanged; + Stream? _onConnectivityChanged; /// Fires whenever the connectivity state changes. Stream get onConnectivityChanged { if (_onConnectivityChanged == null) { - _onConnectivityChanged = eventChannel - .receiveBroadcastStream() - .map((dynamic result) => result.toString()) - .map(parseConnectivityResult); + _onConnectivityChanged = + eventChannel.receiveBroadcastStream().map((dynamic result) { + return result != null ? result.toString() : ''; + }).map(parseConnectivityResult); } - return _onConnectivityChanged; + return _onConnectivityChanged!; } @override - Future checkConnectivity() { - return methodChannel - .invokeMethod('check') - .then(parseConnectivityResult); + Future checkConnectivity() async { + final String checkResult = + await methodChannel.invokeMethod('check') ?? ''; + return parseConnectivityResult(checkResult); } @override - Future getWifiName() async { - String wifiName = await methodChannel.invokeMethod('wifiName'); + Future getWifiName() async { + String? wifiName = await methodChannel.invokeMethod('wifiName'); // as Android might return , uniforming result // our iOS implementation will return null if (wifiName == '') { @@ -54,29 +54,31 @@ class MethodChannelConnectivity extends ConnectivityPlatform { } @override - Future getWifiBSSID() { + Future getWifiBSSID() { return methodChannel.invokeMethod('wifiBSSID'); } @override - Future getWifiIP() { + Future getWifiIP() { return methodChannel.invokeMethod('wifiIPAddress'); } @override Future requestLocationServiceAuthorization({ bool requestAlwaysLocationUsage = false, - }) { - return methodChannel.invokeMethod( - 'requestLocationServiceAuthorization', [ - requestAlwaysLocationUsage - ]).then(parseLocationAuthorizationStatus); + }) async { + final String requestLocationServiceResult = await methodChannel + .invokeMethod('requestLocationServiceAuthorization', + [requestAlwaysLocationUsage]) ?? + ''; + return parseLocationAuthorizationStatus(requestLocationServiceResult); } @override - Future getLocationServiceAuthorization() { - return methodChannel - .invokeMethod('getLocationServiceAuthorization') - .then(parseLocationAuthorizationStatus); + Future getLocationServiceAuthorization() async { + final String getLocationServiceResult = await methodChannel + .invokeMethod('getLocationServiceAuthorization') ?? + ''; + return parseLocationAuthorizationStatus(getLocationServiceResult); } } diff --git a/packages/connectivity/connectivity_platform_interface/pubspec.yaml b/packages/connectivity/connectivity_platform_interface/pubspec.yaml index 5bafcf9b806b..114915a10b60 100644 --- a/packages/connectivity/connectivity_platform_interface/pubspec.yaml +++ b/packages/connectivity/connectivity_platform_interface/pubspec.yaml @@ -3,19 +3,19 @@ description: A common platform interface for the connectivity plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_platform_interface # 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: 1.0.6 +version: 2.0.0-nullsafety.1 dependencies: flutter: sdk: flutter - meta: ^1.0.5 - plugin_platform_interface: ^1.0.1 + meta: ^1.3.0-nullsafety.3 + plugin_platform_interface: ^1.1.0-nullsafety.1 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/connectivity/connectivity_platform_interface/test/method_channel_connectivity_test.dart b/packages/connectivity/connectivity_platform_interface/test/method_channel_connectivity_test.dart index 3d9c405c30ab..0c30530fc9ab 100644 --- a/packages/connectivity/connectivity_platform_interface/test/method_channel_connectivity_test.dart +++ b/packages/connectivity/connectivity_platform_interface/test/method_channel_connectivity_test.dart @@ -12,7 +12,7 @@ void main() { group('$MethodChannelConnectivity', () { final List log = []; - MethodChannelConnectivity methodChannelConnectivity; + late MethodChannelConnectivity methodChannelConnectivity; setUp(() async { methodChannelConnectivity = MethodChannelConnectivity(); @@ -42,7 +42,7 @@ void main() { .setMockMethodCallHandler((MethodCall methodCall) async { switch (methodCall.method) { case 'listen': - await ServicesBinding.instance.defaultBinaryMessenger + await ServicesBinding.instance!.defaultBinaryMessenger .handlePlatformMessage( methodChannelConnectivity.eventChannel.name, methodChannelConnectivity.eventChannel.codec @@ -64,7 +64,7 @@ void main() { }); test('getWifiName', () async { - final String result = await methodChannelConnectivity.getWifiName(); + final String? result = await methodChannelConnectivity.getWifiName(); expect(result, '1337wifi'); expect( log, @@ -78,7 +78,7 @@ void main() { }); test('getWifiBSSID', () async { - final String result = await methodChannelConnectivity.getWifiBSSID(); + final String? result = await methodChannelConnectivity.getWifiBSSID(); expect(result, 'c0:ff:33:c0:d3:55'); expect( log, @@ -92,7 +92,7 @@ void main() { }); test('getWifiIP', () async { - final String result = await methodChannelConnectivity.getWifiIP(); + final String? result = await methodChannelConnectivity.getWifiIP(); expect(result, '127.0.0.1'); expect( log, diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md index 3b5ae7756a98..5ad91979dc89 100644 --- a/packages/cross_file/CHANGELOG.md +++ b/packages/cross_file/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.2.0 + +* **breaking change** Make sure the `saveTo` method returns a `Future` so it can be awaited and users are sure the file has been written to disk. + +## 0.1.0+2 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.1.0+1 + +- Update Flutter SDK constraint. + ## 0.1.0 - Initial open-source release \ No newline at end of file diff --git a/packages/cross_file/README.md b/packages/cross_file/README.md index f1ab89bc52f1..65bd41896184 100644 --- a/packages/cross_file/README.md +++ b/packages/cross_file/README.md @@ -24,7 +24,7 @@ final fileContent = await file.readAsString(); print('Content of the file: ${fileContent}'); // e.g. "Moto G (4)" ``` -You will find links to the API docs on the [pub page](https://pub.dartlang.org/packages/cross_file). +You will find links to the API docs on the [pub page](https://pub.dev/packages/cross_file). ## Getting Started diff --git a/packages/cross_file/lib/src/types/base.dart b/packages/cross_file/lib/src/types/base.dart index 6dc2d51b08b1..1a1b5694d58f 100644 --- a/packages/cross_file/lib/src/types/base.dart +++ b/packages/cross_file/lib/src/types/base.dart @@ -18,7 +18,7 @@ abstract class XFileBase { XFileBase(String path); /// Save the CrossFile at the indicated file path. - void saveTo(String path) async { + Future saveTo(String path) { throw UnimplementedError('saveTo has not been implemented.'); } diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart index 269f2a8d9412..646939612d75 100644 --- a/packages/cross_file/lib/src/types/html.dart +++ b/packages/cross_file/lib/src/types/html.dart @@ -108,7 +108,7 @@ class XFile extends XFileBase { /// Saves the data of this CrossFile at the location indicated by path. /// For the web implementation, the path variable is ignored. - void saveTo(String path) async { + Future saveTo(String path) async { // Create a DOM container where we can host the anchor. _target = ensureInitialized('__x_file_dom_element'); diff --git a/packages/cross_file/lib/src/types/io.dart b/packages/cross_file/lib/src/types/io.dart index 81b8cdd84d67..d9a93559b507 100644 --- a/packages/cross_file/lib/src/types/io.dart +++ b/packages/cross_file/lib/src/types/io.dart @@ -57,7 +57,7 @@ class XFile extends XFileBase { } @override - void saveTo(String path) async { + Future saveTo(String path) async { File fileToSave = File(path); await fileToSave.writeAsBytes(_bytes ?? (await readAsBytes())); await fileToSave.create(); diff --git a/packages/cross_file/pubspec.yaml b/packages/cross_file/pubspec.yaml index 40084d3d1ea0..4c9acf9b008a 100644 --- a/packages/cross_file/pubspec.yaml +++ b/packages/cross_file/pubspec.yaml @@ -1,8 +1,7 @@ - name: cross_file description: An abstraction to allow working with files across multiple platforms. homepage: https://github.com/flutter/plugins/tree/master/packages/cross_file -version: 0.1.0 +version: 0.2.0 dependencies: flutter: @@ -17,4 +16,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.22.0 <2.0.0" \ No newline at end of file + flutter: ">=1.22.0" diff --git a/packages/device_info/device_info/CHANGELOG.md b/packages/device_info/device_info/CHANGELOG.md index 06b327f4a198..910d265b7c3e 100644 --- a/packages/device_info/device_info/CHANGELOG.md +++ b/packages/device_info/device_info/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.0.0-nullsafety.2 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 2.0.0-nullsafety.1 + +* Bump Dart SDK to support null safety. + +## 2.0.0-nullsafety + +* Migrate to null safety. + +## 1.0.1 + +* Update Flutter SDK constraint. + ## 1.0.0 * Announce 1.0.0. diff --git a/packages/device_info/device_info/README.md b/packages/device_info/device_info/README.md index 128bea5c2a17..98bfc15c7156 100644 --- a/packages/device_info/device_info/README.md +++ b/packages/device_info/device_info/README.md @@ -21,11 +21,11 @@ IosDeviceInfo iosInfo = await deviceInfo.iosInfo; print('Running on ${iosInfo.utsname.machine}'); // e.g. "iPod7,1" ``` -You will find links to the API docs on the [pub page](https://pub.dartlang.org/packages/device_info). +You will find links to the API docs on the [pub page](https://pub.dev/packages/device_info). ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/device_info/device_info/example/integration_test/device_info_test.dart b/packages/device_info/device_info/example/integration_test/device_info_test.dart index 2fd1d9a9a491..61c4396b0d8e 100644 --- a/packages/device_info/device_info/example/integration_test/device_info_test.dart +++ b/packages/device_info/device_info/example/integration_test/device_info_test.dart @@ -2,6 +2,9 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// TODO(cyanglaz): Remove once https://github.com/flutter/plugins/pull/3158 is landed. +// @dart = 2.9 + import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:device_info/device_info.dart'; diff --git a/packages/device_info/device_info/example/lib/main.dart b/packages/device_info/device_info/example/lib/main.dart index 63912b37c3ce..805de1417f15 100644 --- a/packages/device_info/device_info/example/lib/main.dart +++ b/packages/device_info/device_info/example/lib/main.dart @@ -36,7 +36,7 @@ class _MyAppState extends State { } Future initPlatformState() async { - Map deviceData; + Map deviceData = {}; try { if (Platform.isAndroid) { diff --git a/packages/device_info/device_info/example/pubspec.yaml b/packages/device_info/device_info/example/pubspec.yaml index 58d54cba6d70..09567fd967b2 100644 --- a/packages/device_info/device_info/example/pubspec.yaml +++ b/packages/device_info/device_info/example/pubspec.yaml @@ -12,11 +12,11 @@ dev_dependencies: sdk: flutter integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 flutter: uses-material-design: true environment: - sdk: ">=2.1.0<3.0.0" + sdk: ">=2.10.0-56.0.dev <3.0.0" flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/packages/device_info/device_info/example/test_driver/integration_test.dart b/packages/device_info/device_info/example/test_driver/integration_test.dart index 7a2c21338786..13327bb884c9 100644 --- a/packages/device_info/device_info/example/test_driver/integration_test.dart +++ b/packages/device_info/device_info/example/test_driver/integration_test.dart @@ -2,6 +2,9 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// TODO(cyanglaz): Remove once https://github.com/flutter/flutter/issues/59879 is fixed. +// @dart = 2.9 + import 'dart:async'; import 'dart:convert'; import 'dart:io'; diff --git a/packages/device_info/device_info/lib/device_info.dart b/packages/device_info/device_info/lib/device_info.dart index f63730c4323f..bccc3d2fbfa6 100644 --- a/packages/device_info/device_info/lib/device_info.dart +++ b/packages/device_info/device_info/lib/device_info.dart @@ -15,7 +15,7 @@ class DeviceInfoPlugin { DeviceInfoPlugin(); /// This information does not change from call to call. Cache it. - AndroidDeviceInfo _cachedAndroidDeviceInfo; + AndroidDeviceInfo? _cachedAndroidDeviceInfo; /// Information derived from `android.os.Build`. /// @@ -25,7 +25,7 @@ class DeviceInfoPlugin { await DeviceInfoPlatform.instance.androidInfo(); /// This information does not change from call to call. Cache it. - IosDeviceInfo _cachedIosDeviceInfo; + IosDeviceInfo? _cachedIosDeviceInfo; /// Information derived from `UIDevice`. /// diff --git a/packages/device_info/device_info/pubspec.yaml b/packages/device_info/device_info/pubspec.yaml index 5b00750fcbbc..1a222869a632 100644 --- a/packages/device_info/device_info/pubspec.yaml +++ b/packages/device_info/device_info/pubspec.yaml @@ -2,7 +2,10 @@ name: device_info description: Flutter plugin providing detailed information about the device (make, model, etc.), and Android or iOS version the app is running on. homepage: https://github.com/flutter/plugins/tree/master/packages/device_info -version: 1.0.0 +# 0.4.y+z is compatible with 1.0.0, if you land a breaking change bump +# the version to 2.0.0. +# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 +version: 2.0.0-nullsafety.2 flutter: plugin: @@ -16,14 +19,13 @@ flutter: dependencies: flutter: sdk: flutter - device_info_platform_interface: ^1.0.0 - + device_info_platform_interface: ^2.0.0-nullsafety.1 dev_dependencies: - test: ^1.3.0 + test: ^1.10.0-nullsafety.1 flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 environment: - sdk: ">=2.1.0<3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/device_info/device_info_platform_interface/CHANGELOG.md b/packages/device_info/device_info_platform_interface/CHANGELOG.md index 8a7eb6c46be3..d4bc81e0f0aa 100644 --- a/packages/device_info/device_info_platform_interface/CHANGELOG.md +++ b/packages/device_info/device_info_platform_interface/CHANGELOG.md @@ -1,3 +1,20 @@ +## 2.0.0-nullsafety.2 + +* Make `baseOS`, `previewSdkInt`, and `securityPatch` nullable types. +* Remove default values for non-nullable types. + +## 2.0.0-nullsafety.1 + +* Bump Dart SDK to support null safety. + +## 2.0.0-nullsafety + +* Migrate to null safety. + +## 1.0.2 + +- Update Flutter SDK constraint. + ## 1.0.1 - Documentation typo fixed. diff --git a/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart b/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart index 5b326cc5350a..4fb940c3effa 100644 --- a/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart +++ b/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart @@ -8,28 +8,28 @@ class AndroidDeviceInfo { /// Android device Info class. AndroidDeviceInfo({ - this.version, - this.board, - this.bootloader, - this.brand, - this.device, - this.display, - this.fingerprint, - this.hardware, - this.host, - this.id, - this.manufacturer, - this.model, - this.product, - List supported32BitAbis, - List supported64BitAbis, - List supportedAbis, - this.tags, - this.type, - this.isPhysicalDevice, - this.androidId, - List systemFeatures, - }) : supported32BitAbis = List.unmodifiable(supported32BitAbis), + required this.version, + required this.board, + required this.bootloader, + required this.brand, + required this.device, + required this.display, + required this.fingerprint, + required this.hardware, + required this.host, + required this.id, + required this.manufacturer, + required this.model, + required this.product, + required List supported32BitAbis, + required List supported64BitAbis, + required List supportedAbis, + required this.tags, + required this.type, + required this.isPhysicalDevice, + required this.androidId, + required List systemFeatures, + }) : supported32BitAbis = List.unmodifiable(supported32BitAbis), supported64BitAbis = List.unmodifiable(supported64BitAbis), supportedAbis = List.unmodifiable(supportedAbis), systemFeatures = List.unmodifiable(systemFeatures); @@ -113,28 +113,28 @@ class AndroidDeviceInfo { /// Deserializes from the message received from [_kChannel]. static AndroidDeviceInfo fromMap(Map map) { return AndroidDeviceInfo( - version: AndroidBuildVersion._fromMap( - map['version']?.cast() ?? {}), - board: map['board'], - bootloader: map['bootloader'], - brand: map['brand'], - device: map['device'], - display: map['display'], - fingerprint: map['fingerprint'], - hardware: map['hardware'], - host: map['host'], - id: map['id'], - manufacturer: map['manufacturer'], - model: map['model'], - product: map['product'], - supported32BitAbis: _fromList(map['supported32BitAbis'] ?? []), - supported64BitAbis: _fromList(map['supported64BitAbis'] ?? []), - supportedAbis: _fromList(map['supportedAbis'] ?? []), - tags: map['tags'], - type: map['type'], - isPhysicalDevice: map['isPhysicalDevice'], - androidId: map['androidId'], - systemFeatures: _fromList(map['systemFeatures'] ?? []), + version: + AndroidBuildVersion._fromMap(map['version']!.cast()), + board: map['board']!, + bootloader: map['bootloader']!, + brand: map['brand']!, + device: map['device']!, + display: map['display']!, + fingerprint: map['fingerprint']!, + hardware: map['hardware']!, + host: map['host']!, + id: map['id']!, + manufacturer: map['manufacturer']!, + model: map['model']!, + product: map['product']!, + supported32BitAbis: _fromList(map['supported32BitAbis']!), + supported64BitAbis: _fromList(map['supported64BitAbis']!), + supportedAbis: _fromList(map['supportedAbis']!), + tags: map['tags']!, + type: map['type']!, + isPhysicalDevice: map['isPhysicalDevice']!, + androidId: map['androidId']!, + systemFeatures: _fromList(map['systemFeatures']!), ); } @@ -152,16 +152,25 @@ class AndroidDeviceInfo { class AndroidBuildVersion { AndroidBuildVersion._({ this.baseOS, - this.codename, - this.incremental, this.previewSdkInt, - this.release, - this.sdkInt, this.securityPatch, + required this.codename, + required this.incremental, + required this.release, + required this.sdkInt, }); /// The base OS build the product is based on. - final String baseOS; + /// This is only available on Android 6.0 or above. + String? baseOS; + + /// The developer preview revision of a prerelease SDK. + /// This is only available on Android 6.0 or above. + int? previewSdkInt; + + /// The user-visible security patch level. + /// This is only available on Android 6.0 or above. + final String? securityPatch; /// The current development codename, or the string "REL" if this is a release build. final String codename; @@ -169,9 +178,6 @@ class AndroidBuildVersion { /// The internal value used by the underlying source control to represent this build. final String incremental; - /// The developer preview revision of a prerelease SDK. - final int previewSdkInt; - /// The user-visible version string. final String release; @@ -180,19 +186,16 @@ class AndroidBuildVersion { /// Possible values are defined in: https://developer.android.com/reference/android/os/Build.VERSION_CODES.html final int sdkInt; - /// The user-visible security patch level. - final String securityPatch; - /// Deserializes from the map message received from [_kChannel]. static AndroidBuildVersion _fromMap(Map map) { return AndroidBuildVersion._( baseOS: map['baseOS'], - codename: map['codename'], - incremental: map['incremental'], previewSdkInt: map['previewSdkInt'], - release: map['release'], - sdkInt: map['sdkInt'], securityPatch: map['securityPatch'], + codename: map['codename']!, + incremental: map['incremental']!, + release: map['release']!, + sdkInt: map['sdkInt']!, ); } } diff --git a/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart b/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart index d41202492101..eb6e5874073b 100644 --- a/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart +++ b/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart @@ -8,14 +8,14 @@ class IosDeviceInfo { /// IOS device info class. IosDeviceInfo({ - this.name, - this.systemName, - this.systemVersion, - this.model, - this.localizedModel, - this.identifierForVendor, - this.isPhysicalDevice, - this.utsname, + required this.name, + required this.systemName, + required this.systemVersion, + required this.model, + required this.localizedModel, + required this.identifierForVendor, + required this.isPhysicalDevice, + required this.utsname, }); /// Device name. @@ -45,15 +45,14 @@ class IosDeviceInfo { /// Deserializes from the map message received from [_kChannel]. static IosDeviceInfo fromMap(Map map) { return IosDeviceInfo( - name: map['name'], - systemName: map['systemName'], - systemVersion: map['systemVersion'], - model: map['model'], - localizedModel: map['localizedModel'], - identifierForVendor: map['identifierForVendor'], + name: map['name']!, + systemName: map['systemName']!, + systemVersion: map['systemVersion']!, + model: map['model']!, + localizedModel: map['localizedModel']!, + identifierForVendor: map['identifierForVendor']!, isPhysicalDevice: map['isPhysicalDevice'] == 'true', - utsname: - IosUtsname._fromMap(map['utsname']?.cast() ?? {}), + utsname: IosUtsname._fromMap(map['utsname']!.cast()), ); } } @@ -62,11 +61,11 @@ class IosDeviceInfo { /// See http://pubs.opengroup.org/onlinepubs/7908799/xsh/sysutsname.h.html for details. class IosUtsname { IosUtsname._({ - this.sysname, - this.nodename, - this.release, - this.version, - this.machine, + required this.sysname, + required this.nodename, + required this.release, + required this.version, + required this.machine, }); /// Operating system name. @@ -87,11 +86,11 @@ class IosUtsname { /// Deserializes from the map message received from [_kChannel]. static IosUtsname _fromMap(Map map) { return IosUtsname._( - sysname: map['sysname'], - nodename: map['nodename'], - release: map['release'], - version: map['version'], - machine: map['machine'], + sysname: map['sysname']!, + nodename: map['nodename']!, + release: map['release']!, + version: map['version']!, + machine: map['machine']!, ); } } diff --git a/packages/device_info/device_info_platform_interface/pubspec.yaml b/packages/device_info/device_info_platform_interface/pubspec.yaml index 656e5b24c373..ca72cc753b63 100644 --- a/packages/device_info/device_info_platform_interface/pubspec.yaml +++ b/packages/device_info/device_info_platform_interface/pubspec.yaml @@ -3,20 +3,20 @@ description: A common platform interface for the device_info plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/device_info # 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: 1.0.1 +version: 2.0.0-nullsafety.2 dependencies: flutter: sdk: flutter - meta: ^1.1.8 - plugin_platform_interface: ^1.0.2 + meta: ^1.3.0-nullsafety.3 + plugin_platform_interface: ^1.1.0-nullsafety.1 dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 + test: ^1.10.0-nullsafety.1 + pedantic: ^1.10.0-nullsafety.1 environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.9.1+hotfix.4" diff --git a/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart b/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart index 1da52e2cf39f..15963854ab12 100644 --- a/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart +++ b/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart @@ -2,11 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(cyanglaz): Remove once https://github.com/flutter/flutter/issues/59879 is fixed. +// @dart = 2.9 + import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - import 'package:device_info_platform_interface/device_info_platform_interface.dart'; - import 'package:device_info_platform_interface/method_channel/method_channel_device_info.dart'; void main() { @@ -23,11 +24,68 @@ void main() { switch (methodCall.method) { case 'getAndroidDeviceInfo': return ({ - "brand": "Google", + "version": { + "securityPatch": "2018-09-05", + "sdkInt": 28, + "release": "9", + "previewSdkInt": 0, + "incremental": "5124027", + "codename": "REL", + "baseOS": "", + }, + "board": "goldfish_x86_64", + "bootloader": "unknown", + "brand": "google", + "device": "generic_x86_64", + "display": "PSR1.180720.075", + "fingerprint": + "google/sdk_gphone_x86_64/generic_x86_64:9/PSR1.180720.075/5124027:user/release-keys", + "hardware": "ranchu", + "host": "abfarm730", + "id": "PSR1.180720.075", + "manufacturer": "Google", + "model": "Android SDK built for x86_64", + "product": "sdk_gphone_x86_64", + "supported32BitAbis": [ + "x86", + ], + "supported64BitAbis": [ + "x86_64", + ], + "supportedAbis": [ + "x86_64", + "x86", + ], + "tags": "release-keys", + "type": "user", + "isPhysicalDevice": false, + "androidId": "f47571f3b4648f45", + "systemFeatures": [ + "android.hardware.sensor.proximity", + "android.software.adoptable_storage", + "android.hardware.sensor.accelerometer", + "android.hardware.faketouch", + "android.software.backup", + "android.hardware.touchscreen", + ], }); case 'getIosDeviceInfo': return ({ - "name": "iPhone 10", + "name": "iPhone 13", + "systemName": "iOS", + "systemVersion": "13.0", + "model": "iPhone", + "localizedModel": "iPhone", + "identifierForVendor": "88F59280-55AD-402C-B922-3203B4794C06", + "isPhysicalDevice": false, + "utsname": { + "sysname": "Darwin", + "nodename": "host", + "release": "19.6.0", + "version": + "Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64", + "machine": "x86_64", + } }); default: return null; @@ -38,12 +96,66 @@ void main() { test("androidInfo", () async { final AndroidDeviceInfo result = await methodChannelDeviceInfo.androidInfo(); - expect(result.brand, "Google"); + + expect(result.version.securityPatch, "2018-09-05"); + expect(result.version.sdkInt, 28); + expect(result.version.release, "9"); + expect(result.version.previewSdkInt, 0); + expect(result.version.incremental, "5124027"); + expect(result.version.codename, "REL"); + expect(result.board, "goldfish_x86_64"); + expect(result.bootloader, "unknown"); + expect(result.brand, "google"); + expect(result.device, "generic_x86_64"); + expect(result.display, "PSR1.180720.075"); + expect(result.fingerprint, + "google/sdk_gphone_x86_64/generic_x86_64:9/PSR1.180720.075/5124027:user/release-keys"); + expect(result.hardware, "ranchu"); + expect(result.host, "abfarm730"); + expect(result.id, "PSR1.180720.075"); + expect(result.manufacturer, "Google"); + expect(result.model, "Android SDK built for x86_64"); + expect(result.product, "sdk_gphone_x86_64"); + expect(result.supported32BitAbis, [ + "x86", + ]); + expect(result.supported64BitAbis, [ + "x86_64", + ]); + expect(result.supportedAbis, [ + "x86_64", + "x86", + ]); + expect(result.tags, "release-keys"); + expect(result.type, "user"); + expect(result.isPhysicalDevice, false); + expect(result.androidId, "f47571f3b4648f45"); + expect(result.systemFeatures, [ + "android.hardware.sensor.proximity", + "android.software.adoptable_storage", + "android.hardware.sensor.accelerometer", + "android.hardware.faketouch", + "android.software.backup", + "android.hardware.touchscreen", + ]); }); test("iosInfo", () async { final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); - expect(result.name, "iPhone 10"); + expect(result.name, "iPhone 13"); + expect(result.systemName, "iOS"); + expect(result.systemVersion, "13.0"); + expect(result.model, "iPhone"); + expect(result.localizedModel, "iPhone"); + expect( + result.identifierForVendor, "88F59280-55AD-402C-B922-3203B4794C06"); + expect(result.isPhysicalDevice, false); + expect(result.utsname.sysname, "Darwin"); + expect(result.utsname.nodename, "host"); + expect(result.utsname.release, "19.6.0"); + expect(result.utsname.version, + "Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64"); + expect(result.utsname.machine, "x86_64"); }); }); } diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index a9d61f1178c2..fe43202b7654 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.0.1+9 + +* Update Flutter SDK constraint. + +## 0.0.1+8 + +* Android: Handle deprecation & unchecked warning as error. + ## 0.0.1+7 * Update android compileSdkVersion to 29. diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/EspressoFlutter.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/EspressoFlutter.java index 106436f2b9ce..6730a66a5406 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/EspressoFlutter.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/EspressoFlutter.java @@ -130,6 +130,7 @@ public WidgetInteraction check(@Nonnull WidgetAssertion assertion) { return this; } + @SuppressWarnings("unchecked") private T performInternal(FlutterAction flutterAction) { checkNotNull( flutterAction, diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterViewAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterViewAction.java index 7864b43d9ec0..fc554d761db5 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterViewAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterViewAction.java @@ -32,7 +32,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import io.flutter.embedding.android.FlutterView; -import io.flutter.view.FlutterNativeView; +import io.flutter.embedding.engine.FlutterJNI; import java.net.URI; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -105,7 +105,7 @@ public void perform(UiController uiController, View flutterView) { // The url {@code FlutterNativeView} returns is the http url that the Dart VM Observatory http // server serves at. Need to convert to the one that the WebSocket uses. URI dartVmServiceProtocolUrl = - DartVmServiceUtil.getServiceProtocolUri(FlutterNativeView.getObservatoryUri()); + DartVmServiceUtil.getServiceProtocolUri(FlutterJNI.getObservatoryUri()); String isolateId = DartVmServiceUtil.getDartIsolateId(flutterView); final FlutterTestingProtocol flutterTestingProtocol = new DartVmService( @@ -199,6 +199,7 @@ public String getName() { return FlutterViewRenderedIdlingResource.class.getSimpleName(); } + @SuppressWarnings("deprecation") @Override public boolean isIdleNow() { boolean isIdle = false; diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java index da11fcc8c8b6..c6ec0e08b03a 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java @@ -30,7 +30,6 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -360,10 +359,9 @@ boolean isTestingApiRegistered(JsonRpcResponse isolateInfoResp) { isolateId, isolateInfoResp.getError())); return false; } - Iterator extensions = - isolateInfoResp.getResult().get(EXTENSION_RPCS_TAG).getAsJsonArray().iterator(); - while (extensions.hasNext()) { - String extensionApi = extensions.next().getAsString(); + for (JsonElement jsonElement : + isolateInfoResp.getResult().get(EXTENSION_RPCS_TAG).getAsJsonArray()) { + String extensionApi = jsonElement.getAsString(); if (TESTING_EXTENSION_METHOD.equals(extensionApi)) { Log.d( TAG, diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmServiceUtil.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmServiceUtil.java index 2cf41f1f87a7..a943194e348e 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmServiceUtil.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmServiceUtil.java @@ -71,6 +71,7 @@ public static String getDartIsolateId(View flutterView) { } /** Gets the Dart executor for the given {@code flutterView}. */ + @SuppressWarnings("deprecation") public static DartExecutor getDartExecutor(View flutterView) { checkNotNull(flutterView, "The Flutter View instance cannot be null."); // Flutter's embedding is in the phase of rewriting/refactoring. Let's be compatible with both diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/FlutterMatchers.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/FlutterMatchers.java index 5a272f24bdc0..f867b7dcbed4 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/FlutterMatchers.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/FlutterMatchers.java @@ -96,6 +96,7 @@ public void describeTo(Description description) { description.appendText("is a FlutterView"); } + @SuppressWarnings("deprecation") @Override public boolean matchesSafely(View flutterView) { return flutterView instanceof FlutterView diff --git a/packages/espresso/example/android/app/build.gradle b/packages/espresso/example/android/app/build.gradle index 45fe0e173fe1..6def13f65898 100644 --- a/packages/espresso/example/android/app/build.gradle +++ b/packages/espresso/example/android/app/build.gradle @@ -35,7 +35,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.espresso_example" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml index fe284260b73c..e7e1f6691ed3 100644 --- a/packages/espresso/pubspec.yaml +++ b/packages/espresso/pubspec.yaml @@ -1,11 +1,11 @@ name: espresso description: Java classes for testing Flutter apps using Espresso. -version: 0.0.1+7 +version: 0.0.1+9 homepage: https://github.com/flutter/plugins/espresso environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + flutter: ">=1.10.0" dependencies: flutter: diff --git a/packages/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md index 8dab88a33cef..fe01ffec47b6 100644 --- a/packages/file_selector/file_selector/CHANGELOG.md +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.7.0+2 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.7.0+1 + +* Update Flutter SDK constraint. + ## 0.7.0 * Initial Open Source release. diff --git a/packages/file_selector/file_selector/example/lib/get_directory_page.dart b/packages/file_selector/file_selector/example/lib/get_directory_page.dart index 2afc58f246a4..6463fb532957 100644 --- a/packages/file_selector/file_selector/example/lib/get_directory_page.dart +++ b/packages/file_selector/file_selector/example/lib/get_directory_page.dart @@ -24,9 +24,11 @@ class GetDirectoryPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - RaisedButton( - color: Colors.blue, - textColor: Colors.white, + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), child: Text('Press to ask user to choose a directory'), onPressed: () => _getDirectoryPath(context), ), @@ -55,7 +57,7 @@ class TextDisplay extends StatelessWidget { ), ), actions: [ - FlatButton( + TextButton( child: const Text('Close'), onPressed: () => Navigator.pop(context), ), diff --git a/packages/file_selector/file_selector/example/lib/home_page.dart b/packages/file_selector/file_selector/example/lib/home_page.dart index c37d90170f6e..1cb7ef261e88 100644 --- a/packages/file_selector/file_selector/example/lib/home_page.dart +++ b/packages/file_selector/file_selector/example/lib/home_page.dart @@ -4,6 +4,10 @@ import 'package:flutter/material.dart'; class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { + final ButtonStyle style = ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ); return Scaffold( appBar: AppBar( title: Text('File Selector Demo Home Page'), @@ -12,37 +16,32 @@ class HomePage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - RaisedButton( - color: Colors.blue, - textColor: Colors.white, + ElevatedButton( + style: style, child: Text('Open a text file'), onPressed: () => Navigator.pushNamed(context, '/open/text'), ), SizedBox(height: 10), - RaisedButton( - color: Colors.blue, - textColor: Colors.white, + ElevatedButton( + style: style, child: Text('Open an image'), onPressed: () => Navigator.pushNamed(context, '/open/image'), ), SizedBox(height: 10), - RaisedButton( - color: Colors.blue, - textColor: Colors.white, + ElevatedButton( + style: style, child: Text('Open multiple images'), onPressed: () => Navigator.pushNamed(context, '/open/images'), ), SizedBox(height: 10), - RaisedButton( - color: Colors.blue, - textColor: Colors.white, + ElevatedButton( + style: style, child: Text('Save a file'), onPressed: () => Navigator.pushNamed(context, '/save/text'), ), SizedBox(height: 10), - RaisedButton( - color: Colors.blue, - textColor: Colors.white, + ElevatedButton( + style: style, child: Text('Open a get directory dialog'), onPressed: () => Navigator.pushNamed(context, '/directory'), ), diff --git a/packages/file_selector/file_selector/example/lib/open_image_page.dart b/packages/file_selector/file_selector/example/lib/open_image_page.dart index 2821635fb30b..593a1d60aed8 100644 --- a/packages/file_selector/file_selector/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector/example/lib/open_image_page.dart @@ -31,9 +31,11 @@ class OpenImagePage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - RaisedButton( - color: Colors.blue, - textColor: Colors.white, + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), child: Text('Press to open an image file(png, jpg)'), onPressed: () => _openImageFile(context), ), @@ -63,7 +65,7 @@ class ImageDisplay extends StatelessWidget { // while on other platforms it is a system path. content: kIsWeb ? Image.network(filePath) : Image.file(File(filePath)), actions: [ - FlatButton( + TextButton( child: const Text('Close'), onPressed: () { Navigator.pop(context); diff --git a/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart index 7a27a0c0715f..58b59cd91b03 100644 --- a/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart @@ -34,9 +34,11 @@ class OpenMultipleImagesPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - RaisedButton( - color: Colors.blue, - textColor: Colors.white, + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), child: Text('Press to open multiple images (png, jpg)'), onPressed: () => _openImageFile(context), ), @@ -74,7 +76,7 @@ class MultipleImagesDisplay extends StatelessWidget { ), ), actions: [ - FlatButton( + TextButton( child: const Text('Close'), onPressed: () { Navigator.pop(context); diff --git a/packages/file_selector/file_selector/example/lib/open_text_page.dart b/packages/file_selector/file_selector/example/lib/open_text_page.dart index 4cb3064046fd..299d0e2dc21a 100644 --- a/packages/file_selector/file_selector/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector/example/lib/open_text_page.dart @@ -28,9 +28,11 @@ class OpenTextPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - RaisedButton( - color: Colors.blue, - textColor: Colors.white, + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), child: Text('Press to open a text file (json, txt)'), onPressed: () => _openTextFile(context), ), @@ -62,7 +64,7 @@ class TextDisplay extends StatelessWidget { ), ), actions: [ - FlatButton( + TextButton( child: const Text('Close'), onPressed: () => Navigator.pop(context), ), diff --git a/packages/file_selector/file_selector/example/lib/save_text_page.dart b/packages/file_selector/file_selector/example/lib/save_text_page.dart index b70231f5a410..47408662ecee 100644 --- a/packages/file_selector/file_selector/example/lib/save_text_page.dart +++ b/packages/file_selector/file_selector/example/lib/save_text_page.dart @@ -51,9 +51,11 @@ class SaveTextPage extends StatelessWidget { ), ), SizedBox(height: 10), - RaisedButton( - color: Colors.blue, - textColor: Colors.white, + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), child: Text('Press to save a text file'), onPressed: () => _saveFile(), ), diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml index 5a90f048c314..a55b7f4e06e7 100644 --- a/packages/file_selector/file_selector/pubspec.yaml +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -1,7 +1,7 @@ name: file_selector description: Flutter plugin for opening and saving files. homepage: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector -version: 0.7.0 +version: 0.7.0+2 dependencies: flutter: @@ -18,4 +18,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md index 3663064a069f..0d6d6d08c298 100644 --- a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md +++ b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.0.3 + +* Update Flutter SDK constraint. + +## 1.0.2 + +* Replace locally defined `XFile` types with the versions from the [cross_file](https://pub.dev/packages/cross_file) package. + ## 1.0.1 * Allow type groups that allow any file. diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart b/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart index 8681a1dbffa6..586b1abcae1e 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.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. +import 'package:cross_file/cross_file.dart'; import 'package:flutter/services.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart index cf23d5f01eab..e7b32631a58b 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart @@ -4,6 +4,7 @@ import 'dart:async'; +import 'package:cross_file/cross_file.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/types.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/types.dart index 8848c6751ba3..88dc3c2a5f83 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/types.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/types.dart @@ -1,3 +1,2 @@ -export 'x_file/x_file.dart'; - +export 'package:cross_file/cross_file.dart'; export 'x_type_group/x_type_group.dart'; diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/base.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/base.dart deleted file mode 100644 index 7ea050ff28db..000000000000 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/base.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -/// The interface for a XFile. -/// -/// A XFile is a container that wraps the path of a selected -/// file by the user and (in some platforms, like web) the bytes -/// with the contents of the file. -/// -/// This class is a very limited subset of dart:io [File], so all -/// the methods should seem familiar. -abstract class XFileBase { - /// Construct a XFile - XFileBase(String path); - - /// Save the XFile at the indicated file path. - void saveTo(String path) async { - throw UnimplementedError('saveTo has not been implemented.'); - } - - /// Get the path of the picked file. - /// - /// This should only be used as a backwards-compatibility clutch - /// for mobile apps, or cosmetic reasons only (to show the user - /// the path they've picked). - /// - /// Accessing the data contained in the picked file by its path - /// is platform-dependant (and won't work on web), so use the - /// byte getters in the XFile instance instead. - String get path { - throw UnimplementedError('.path has not been implemented.'); - } - - /// The name of the file as it was selected by the user in their device. - /// - /// Use only for cosmetic reasons, do not try to use this as a path. - String get name { - throw UnimplementedError('.name has not been implemented.'); - } - - /// For web, it may be necessary for a file to know its MIME type. - String get mimeType { - throw UnimplementedError('.mimeType has not been implemented.'); - } - - /// Get the length of the file. Returns a `Future` that completes with the length in bytes. - Future length() { - throw UnimplementedError('.length() has not been implemented.'); - } - - /// Synchronously read the entire file contents as a string using the given [Encoding]. - /// - /// By default, `encoding` is [utf8]. - /// - /// Throws Exception if the operation fails. - Future readAsString({Encoding encoding = utf8}) { - throw UnimplementedError('readAsString() has not been implemented.'); - } - - /// Synchronously read the entire file contents as a list of bytes. - /// - /// Throws Exception if the operation fails. - Future readAsBytes() { - throw UnimplementedError('readAsBytes() has not been implemented.'); - } - - /// Create a new independent [Stream] for the contents of this file. - /// - /// If `start` is present, the file will be read from byte-offset `start`. Otherwise from the beginning (index 0). - /// - /// If `end` is present, only up to byte-index `end` will be read. Otherwise, until end of file. - /// - /// In order to make sure that system resources are freed, the stream must be read to completion or the subscription on the stream must be cancelled. - Stream openRead([int start, int end]) { - throw UnimplementedError('openRead() has not been implemented.'); - } - - /// Get the last-modified time for the XFile - Future lastModified() { - throw UnimplementedError('openRead() has not been implemented.'); - } -} diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/html.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/html.dart deleted file mode 100644 index fe898eb4ca62..000000000000 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/html.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:http/http.dart' as http show readBytes; -import 'package:meta/meta.dart'; -import 'dart:html'; - -import '../../web_helpers/web_helpers.dart'; -import './base.dart'; - -/// A XFile that works on web. -/// -/// It wraps the bytes of a selected file. -class XFile extends XFileBase { - String path; - - final String mimeType; - final Uint8List _data; - final int _length; - final String name; - final DateTime _lastModified; - Element _target; - - final XFileTestOverrides _overrides; - - bool get _hasTestOverrides => _overrides != null; - - /// Construct a XFile object from its ObjectUrl. - /// - /// Optionally, this can be initialized with `bytes` and `length` - /// so no http requests are performed to retrieve files later. - /// - /// `name` needs to be passed from the outside, since we only have - /// access to it while we create the ObjectUrl. - XFile( - this.path, { - this.mimeType, - this.name, - int length, - Uint8List bytes, - DateTime lastModified, - @visibleForTesting XFileTestOverrides overrides, - }) : _data = bytes, - _length = length, - _overrides = overrides, - _lastModified = lastModified, - super(path); - - /// Construct an XFile from its data - XFile.fromData( - Uint8List bytes, { - this.mimeType, - this.name, - int length, - DateTime lastModified, - this.path, - @visibleForTesting XFileTestOverrides overrides, - }) : _data = bytes, - _length = length, - _overrides = overrides, - _lastModified = lastModified, - super(path) { - if (path == null) { - final blob = (mimeType == null) ? Blob([bytes]) : Blob([bytes], mimeType); - this.path = Url.createObjectUrl(blob); - } - } - - @override - Future lastModified() async { - if (_lastModified != null) { - return Future.value(_lastModified); - } - return null; - } - - Future get _bytes async { - if (_data != null) { - return Future.value(UnmodifiableUint8ListView(_data)); - } - return http.readBytes(path); - } - - @override - Future length() async { - return _length ?? (await _bytes).length; - } - - @override - Future readAsString({Encoding encoding = utf8}) async { - return encoding.decode(await _bytes); - } - - @override - Future readAsBytes() async { - return Future.value(await _bytes); - } - - @override - Stream openRead([int start, int end]) async* { - final bytes = await _bytes; - yield bytes.sublist(start ?? 0, end ?? bytes.length); - } - - /// Saves the data of this XFile at the location indicated by path. - /// For the web implementation, the path variable is ignored. - void saveTo(String path) async { - // Create a DOM container where we can host the anchor. - _target = ensureInitialized('__x_file_dom_element'); - - // Create an tag with the appropriate download attributes and click it - // May be overridden with XFileTestOverrides - final AnchorElement element = - (_hasTestOverrides && _overrides.createAnchorElement != null) - ? _overrides.createAnchorElement(this.path, this.name) - : createAnchorElement(this.path, this.name); - - // Clear the children in our container so we can add an element to click - _target.children.clear(); - addElementToContainerAndClick(_target, element); - } -} - -/// Overrides some functions to allow testing -@visibleForTesting -class XFileTestOverrides { - /// For overriding the creation of the file input element. - Element Function(String href, String suggestedName) createAnchorElement; - - /// Default constructor for overrides - XFileTestOverrides({this.createAnchorElement}); -} diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/interface.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/interface.dart deleted file mode 100644 index f5fe388e0899..000000000000 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/interface.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'dart:typed_data'; -import 'package:meta/meta.dart'; - -import './base.dart'; - -/// A XFile is a cross-platform, simplified File abstraction. -/// -/// It wraps the bytes of a selected file, and its (platform-dependant) path. -class XFile extends XFileBase { - /// Construct a XFile object from its path. - /// - /// Optionally, this can be initialized with `bytes` and `length` - /// so no http requests are performed to retrieve data later. - /// - /// `name` may be passed from the outside, for those cases where the effective - /// `path` of the file doesn't match what the user sees when selecting it - /// (like in web) - XFile( - String path, { - String mimeType, - String name, - int length, - Uint8List bytes, - DateTime lastModified, - @visibleForTesting XFileTestOverrides overrides, - }) : super(path) { - throw UnimplementedError( - 'XFile is not available in your current platform.'); - } - - /// Construct a XFile object from its data - XFile.fromData( - Uint8List bytes, { - String mimeType, - String name, - int length, - DateTime lastModified, - String path, - @visibleForTesting XFileTestOverrides overrides, - }) : super(path) { - throw UnimplementedError( - 'XFile is not available in your current platform.'); - } -} - -/// Overrides some functions of XFile for testing purposes -@visibleForTesting -class XFileTestOverrides { - /// For overriding the creation of the file input element. - dynamic Function(String href, String suggestedName) createAnchorElement; - - /// Default constructor for overrides - XFileTestOverrides({this.createAnchorElement}); -} diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/io.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/io.dart deleted file mode 100644 index 753732df2811..000000000000 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/io.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import './base.dart'; - -/// A XFile backed by a dart:io File. -class XFile extends XFileBase { - final File _file; - final String mimeType; - final DateTime _lastModified; - int _length; - - final Uint8List _bytes; - - /// Construct a XFile object backed by a dart:io File. - XFile( - String path, { - this.mimeType, - String name, - int length, - Uint8List bytes, - DateTime lastModified, - }) : _file = File(path), - _bytes = null, - _lastModified = lastModified, - super(path); - - /// Construct an XFile from its data - XFile.fromData( - Uint8List bytes, { - this.mimeType, - String path, - String name, - int length, - DateTime lastModified, - }) : _bytes = bytes, - _file = File(path ?? ''), - _length = length, - _lastModified = lastModified, - super(path) { - if (length == null) { - _length = bytes.length; - } - } - - @override - Future lastModified() { - if (_lastModified != null) { - return Future.value(_lastModified); - } - return _file.lastModified(); - } - - @override - void saveTo(String path) async { - File fileToSave = File(path); - await fileToSave.writeAsBytes(_bytes ?? (await readAsBytes())); - await fileToSave.create(); - } - - @override - String get path { - return _file.path; - } - - @override - String get name { - return _file.path.split(Platform.pathSeparator).last; - } - - @override - Future length() { - if (_length != null) { - return Future.value(_length); - } - return _file.length(); - } - - @override - Future readAsString({Encoding encoding = utf8}) { - if (_bytes != null) { - return Future.value(String.fromCharCodes(_bytes)); - } - return _file.readAsString(encoding: encoding); - } - - @override - Future readAsBytes() { - if (_bytes != null) { - return Future.value(_bytes); - } - return _file.readAsBytes(); - } - - Stream _getBytes(int start, int end) async* { - final bytes = _bytes; - yield bytes.sublist(start ?? 0, end ?? bytes.length); - } - - @override - Stream openRead([int start, int end]) { - if (_bytes != null) { - return _getBytes(start, end); - } else { - return _file - .openRead(start ?? 0, end) - .map((chunk) => Uint8List.fromList(chunk)); - } - } -} diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/x_file.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/x_file.dart deleted file mode 100644 index 4545c605875a..000000000000 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/x_file.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'interface.dart' - if (dart.library.html) 'html.dart' - if (dart.library.io) 'io.dart'; diff --git a/packages/file_selector/file_selector_platform_interface/pubspec.yaml b/packages/file_selector/file_selector_platform_interface/pubspec.yaml index ca5c2bd189ae..2d0b7c954ece 100644 --- a/packages/file_selector/file_selector_platform_interface/pubspec.yaml +++ b/packages/file_selector/file_selector_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the file_selector plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector_platform_interface # 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: 1.0.1 +version: 1.0.3 dependencies: flutter: @@ -11,6 +11,7 @@ dependencies: meta: ^1.0.5 http: ^0.12.0+1 plugin_platform_interface: ^1.0.1 + cross_file: ^0.1.0 dev_dependencies: test: ^1.15.0 @@ -21,4 +22,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + flutter: ">=1.9.1+hotfix.4" diff --git a/packages/file_selector/file_selector_platform_interface/test/assets/hello.txt b/packages/file_selector/file_selector_platform_interface/test/assets/hello.txt deleted file mode 100644 index 5dd01c177f5d..000000000000 --- a/packages/file_selector/file_selector_platform_interface/test/assets/hello.txt +++ /dev/null @@ -1 +0,0 @@ -Hello, world! \ No newline at end of file diff --git a/packages/file_selector/file_selector_platform_interface/test/x_file_html_test.dart b/packages/file_selector/file_selector_platform_interface/test/x_file_html_test.dart deleted file mode 100644 index f888a0486ca7..000000000000 --- a/packages/file_selector/file_selector_platform_interface/test/x_file_html_test.dart +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2020 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. - -@TestOn('chrome') // Uses web-only Flutter SDK - -import 'dart:convert'; -import 'dart:html' as html; -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; - -import 'dart:html'; - -final String expectedStringContents = 'Hello, world!'; -final Uint8List bytes = utf8.encode(expectedStringContents); -final html.File textFile = html.File([bytes], 'hello.txt'); -final String textFileUrl = html.Url.createObjectUrl(textFile); - -void main() { - group('Create with an objectUrl', () { - final file = XFile(textFileUrl); - - test('Can be read as a string', () async { - expect(await file.readAsString(), equals(expectedStringContents)); - }); - test('Can be read as bytes', () async { - expect(await file.readAsBytes(), equals(bytes)); - }); - - test('Can be read as a stream', () async { - expect(await file.openRead().first, equals(bytes)); - }); - - test('Stream can be sliced', () async { - expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5))); - }); - }); - - group('Create from data', () { - final file = XFile.fromData(bytes); - - test('Can be read as a string', () async { - expect(await file.readAsString(), equals(expectedStringContents)); - }); - test('Can be read as bytes', () async { - expect(await file.readAsBytes(), equals(bytes)); - }); - - test('Can be read as a stream', () async { - expect(await file.openRead().first, equals(bytes)); - }); - - test('Stream can be sliced', () async { - expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5))); - }); - }); - - group('saveTo(..)', () { - final String xFileDomElementId = '__x_file_dom_element'; - - group('XFile saveTo(..)', () { - test('creates a DOM container', () async { - XFile file = XFile.fromData(bytes); - - await file.saveTo(''); - - final container = querySelector('#${xFileDomElementId}'); - - expect(container, isNotNull); - }); - - test('create anchor element', () async { - XFile file = XFile.fromData(bytes, name: textFile.name); - - await file.saveTo('path'); - - final container = querySelector('#${xFileDomElementId}'); - final AnchorElement element = container?.children?.firstWhere( - (element) => element.tagName == 'A', - orElse: () => null); - - expect(element, isNotNull); - expect(element.href, file.path); - expect(element.download, file.name); - }); - - test('anchor element is clicked', () async { - final mockAnchor = AnchorElement(); - - XFileTestOverrides overrides = XFileTestOverrides( - createAnchorElement: (_, __) => mockAnchor, - ); - - XFile file = - XFile.fromData(bytes, name: textFile.name, overrides: overrides); - - bool clicked = false; - mockAnchor.onClick.listen((event) => clicked = true); - - await file.saveTo('path'); - - expect(clicked, true); - }); - }); - }); -} diff --git a/packages/file_selector/file_selector_platform_interface/test/x_file_io_test.dart b/packages/file_selector/file_selector_platform_interface/test/x_file_io_test.dart deleted file mode 100644 index b669324462b2..000000000000 --- a/packages/file_selector/file_selector_platform_interface/test/x_file_io_test.dart +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2020 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. - -@TestOn('vm') // Uses dart:io - -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; - -// Please note that executing this test with command -// `flutter test test/x_file_io_test.dart` will set the directory -// to ./file_selector_platform_interface. -// -// This will cause our hello.txt file to be not be found. Please -// execute this test with `flutter test` or change the path prefix -// to ./test/assets/ -// -// https://github.com/flutter/flutter/issues/20907 - -final pathPrefix = './assets/'; -final path = pathPrefix + 'hello.txt'; -final String expectedStringContents = 'Hello, world!'; -final Uint8List bytes = utf8.encode(expectedStringContents); -final File textFile = File(path); -final String textFilePath = textFile.path; - -void main() { - group('Create with a path', () { - final file = XFile(textFilePath); - - test('Can be read as a string', () async { - expect(await file.readAsString(), equals(expectedStringContents)); - }); - test('Can be read as bytes', () async { - expect(await file.readAsBytes(), equals(bytes)); - }); - - test('Can be read as a stream', () async { - expect(await file.openRead().first, equals(bytes)); - }); - - test('Stream can be sliced', () async { - expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5))); - }); - - test('saveTo(..) creates file', () async { - File removeBeforeTest = File(pathPrefix + 'newFilePath.txt'); - if (removeBeforeTest.existsSync()) { - await removeBeforeTest.delete(); - } - - await file.saveTo(pathPrefix + 'newFilePath.txt'); - File newFile = File(pathPrefix + 'newFilePath.txt'); - - expect(newFile.existsSync(), isTrue); - expect(newFile.readAsStringSync(), 'Hello, world!'); - - await newFile.delete(); - }); - }); - - group('Create with data', () { - final file = XFile.fromData(bytes); - - test('Can be read as a string', () async { - expect(await file.readAsString(), equals(expectedStringContents)); - }); - test('Can be read as bytes', () async { - expect(await file.readAsBytes(), equals(bytes)); - }); - - test('Can be read as a stream', () async { - expect(await file.openRead().first, equals(bytes)); - }); - - test('Stream can be sliced', () async { - expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5))); - }); - - test('Function saveTo(..) creates file', () async { - File removeBeforeTest = File(pathPrefix + 'newFileData.txt'); - if (removeBeforeTest.existsSync()) { - await removeBeforeTest.delete(); - } - - await file.saveTo(pathPrefix + 'newFileData.txt'); - File newFile = File(pathPrefix + 'newFileData.txt'); - - expect(newFile.existsSync(), isTrue); - expect(newFile.readAsStringSync(), 'Hello, world!'); - - await newFile.delete(); - }); - }); -} diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 401be5f5278a..e1804c5cc7f5 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.0.0-nullsafety.2 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 2.0.0-nullsafety.1 + +* Fix example app SDK. + +## 2.0.0-nullsafety + +* Bump Dart SDK. + +## 1.0.12 + +* Update Flutter SDK constraint. + ## 1.0.11 * Keep handling deprecated Android v1 classes for backward compatibility. diff --git a/packages/flutter_plugin_android_lifecycle/README.md b/packages/flutter_plugin_android_lifecycle/README.md index 25f4d9efd056..3290140f4e5e 100644 --- a/packages/flutter_plugin_android_lifecycle/README.md +++ b/packages/flutter_plugin_android_lifecycle/README.md @@ -1,6 +1,6 @@ # Flutter Android Lifecycle Plugin -[![pub package](https://img.shields.io/pub/v/flutter_plugin_android_lifecycle.svg)](https://pub.dartlang.org/packages/flutter_plugin_android_lifecycle) +[![pub package](https://img.shields.io/pub/v/flutter_plugin_android_lifecycle.svg)](https://pub.dev/packages/flutter_plugin_android_lifecycle) A Flutter plugin for Android to allow other Flutter plugins to access Android `Lifecycle` objects in the plugin's binding. @@ -11,7 +11,7 @@ major version of the Android `Lifecycle` API they expect. ## Installation -Add `flutter_plugin_android_lifecycle` as a [dependency in your pubspec.yaml file](https://flutter.io/using-packages/). +Add `flutter_plugin_android_lifecycle` as a [dependency in your pubspec.yaml file](https://flutter.dev/using-packages/). ## Example diff --git a/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart b/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart index 1405ab69153c..ba67043bcf43 100644 --- a/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart +++ b/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// @dart = 2.9 + import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:flutter_plugin_android_lifecycle_example/main.dart'; diff --git a/packages/flutter_plugin_android_lifecycle/example/lib/main.dart b/packages/flutter_plugin_android_lifecycle/example/lib/main.dart index 6dfe523a0ae1..eb82aacd0237 100644 --- a/packages/flutter_plugin_android_lifecycle/example/lib/main.dart +++ b/packages/flutter_plugin_android_lifecycle/example/lib/main.dart @@ -1,3 +1,7 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. 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 'package:flutter/material.dart'; diff --git a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml index a040dbe95d92..4643317c1951 100644 --- a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml @@ -1,23 +1,21 @@ name: flutter_plugin_android_lifecycle_example description: Demonstrates how to use the flutter_plugin_android_lifecycle plugin. -publish_to: 'none' - -environment: - sdk: ">=2.1.0 <3.0.0" dependencies: flutter: sdk: flutter - integration_test: - path: ../../integration_test + flutter_plugin_android_lifecycle: + path: ../ dev_dependencies: + integration_test: + path: ../../integration_test flutter_test: sdk: flutter pedantic: ^1.8.0 - flutter_plugin_android_lifecycle: - path: ../ +environment: + sdk: ">=2.12.0-0 <3.0.0" flutter: uses-material-design: true diff --git a/packages/flutter_plugin_android_lifecycle/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/pubspec.yaml index a671381b3f47..d94237c2101a 100644 --- a/packages/flutter_plugin_android_lifecycle/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/pubspec.yaml @@ -1,11 +1,11 @@ name: flutter_plugin_android_lifecycle description: Flutter plugin for accessing an Android Lifecycle within other plugins. -version: 1.0.11 +version: 2.0.0-nullsafety.2 homepage: https://github.com/flutter/plugins/tree/master/packages/flutter_plugin_android_lifecycle environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.12.13" dependencies: flutter: diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 12e9ab4b55f7..b23ef79651e7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,15 @@ +## 1.0.10 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 1.0.9 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 1.0.8 + +* Update Flutter SDK constraint. + ## 1.0.7 * Android: Handle deprecation & unchecked warning as error. diff --git a/packages/google_maps_flutter/google_maps_flutter/README.md b/packages/google_maps_flutter/google_maps_flutter/README.md index cbc0ccbf62b8..6bb11a8da793 100644 --- a/packages/google_maps_flutter/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/README.md @@ -1,12 +1,12 @@ # Google Maps for Flutter -[![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dartlang.org/packages/google_maps_flutter) +[![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dev/packages/google_maps_flutter) A Flutter plugin that provides a [Google Maps](https://developers.google.com/maps/) widget. ## Usage -To use this plugin, add `google_maps_flutter` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add `google_maps_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). ## Getting Started diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj index f6a2d6ec291a..75392aeb82e5 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,11 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; @@ -28,8 +24,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -40,14 +34,12 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -63,8 +55,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -83,9 +73,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -230,7 +218,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 74BF216DF17B0C7F983459BD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -270,9 +258,12 @@ files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -285,9 +276,12 @@ files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${PODS_ROOT}/../Flutter/Flutter.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -331,7 +325,6 @@ /* Begin XCBuildConfiguration section */ 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -388,7 +381,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/animate_camera.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/animate_camera.dart index 37c79d302733..ae5e9f6cee3a 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/animate_camera.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/animate_camera.dart @@ -54,7 +54,7 @@ class AnimateCameraState extends State { children: [ Column( children: [ - FlatButton( + TextButton( onPressed: () { mapController.animateCamera( CameraUpdate.newCameraPosition( @@ -69,7 +69,7 @@ class AnimateCameraState extends State { }, child: const Text('newCameraPosition'), ), - FlatButton( + TextButton( onPressed: () { mapController.animateCamera( CameraUpdate.newLatLng( @@ -79,7 +79,7 @@ class AnimateCameraState extends State { }, child: const Text('newLatLng'), ), - FlatButton( + TextButton( onPressed: () { mapController.animateCamera( CameraUpdate.newLatLngBounds( @@ -93,7 +93,7 @@ class AnimateCameraState extends State { }, child: const Text('newLatLngBounds'), ), - FlatButton( + TextButton( onPressed: () { mapController.animateCamera( CameraUpdate.newLatLngZoom( @@ -104,7 +104,7 @@ class AnimateCameraState extends State { }, child: const Text('newLatLngZoom'), ), - FlatButton( + TextButton( onPressed: () { mapController.animateCamera( CameraUpdate.scrollBy(150.0, -225.0), @@ -116,7 +116,7 @@ class AnimateCameraState extends State { ), Column( children: [ - FlatButton( + TextButton( onPressed: () { mapController.animateCamera( CameraUpdate.zoomBy( @@ -127,7 +127,7 @@ class AnimateCameraState extends State { }, child: const Text('zoomBy with focus'), ), - FlatButton( + TextButton( onPressed: () { mapController.animateCamera( CameraUpdate.zoomBy(-0.5), @@ -135,7 +135,7 @@ class AnimateCameraState extends State { }, child: const Text('zoomBy'), ), - FlatButton( + TextButton( onPressed: () { mapController.animateCamera( CameraUpdate.zoomIn(), @@ -143,7 +143,7 @@ class AnimateCameraState extends State { }, child: const Text('zoomIn'), ), - FlatButton( + TextButton( onPressed: () { mapController.animateCamera( CameraUpdate.zoomOut(), @@ -151,7 +151,7 @@ class AnimateCameraState extends State { }, child: const Text('zoomOut'), ), - FlatButton( + TextButton( onPressed: () { mapController.animateCamera( CameraUpdate.zoomTo(16.0), diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart index efdbe016f7c4..f194f8cb3f3b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart @@ -83,7 +83,7 @@ class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { Widget _getVisibleRegionButton() { return Padding( padding: const EdgeInsets.all(8.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Visible Region Bounds'), onPressed: () async { final LatLngBounds visibleRegion = diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart index f117c3a48b22..61a62ac0cf6d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart @@ -70,7 +70,7 @@ class MapUiBodyState extends State { } Widget _compassToggler() { - return FlatButton( + return TextButton( child: Text('${_compassEnabled ? 'disable' : 'enable'} compass'), onPressed: () { setState(() { @@ -81,7 +81,7 @@ class MapUiBodyState extends State { } Widget _mapToolbarToggler() { - return FlatButton( + return TextButton( child: Text('${_mapToolbarEnabled ? 'disable' : 'enable'} map toolbar'), onPressed: () { setState(() { @@ -92,7 +92,7 @@ class MapUiBodyState extends State { } Widget _latLngBoundsToggler() { - return FlatButton( + return TextButton( child: Text( _cameraTargetBounds.bounds == null ? 'bound camera target' @@ -109,7 +109,7 @@ class MapUiBodyState extends State { } Widget _zoomBoundsToggler() { - return FlatButton( + return TextButton( child: Text(_minMaxZoomPreference.minZoom == null ? 'bound zoom' : 'release zoom'), @@ -126,7 +126,7 @@ class MapUiBodyState extends State { Widget _mapTypeCycler() { final MapType nextType = MapType.values[(_mapType.index + 1) % MapType.values.length]; - return FlatButton( + return TextButton( child: Text('change map type to $nextType'), onPressed: () { setState(() { @@ -137,7 +137,7 @@ class MapUiBodyState extends State { } Widget _rotateToggler() { - return FlatButton( + return TextButton( child: Text('${_rotateGesturesEnabled ? 'disable' : 'enable'} rotate'), onPressed: () { setState(() { @@ -148,7 +148,7 @@ class MapUiBodyState extends State { } Widget _scrollToggler() { - return FlatButton( + return TextButton( child: Text('${_scrollGesturesEnabled ? 'disable' : 'enable'} scroll'), onPressed: () { setState(() { @@ -159,7 +159,7 @@ class MapUiBodyState extends State { } Widget _tiltToggler() { - return FlatButton( + return TextButton( child: Text('${_tiltGesturesEnabled ? 'disable' : 'enable'} tilt'), onPressed: () { setState(() { @@ -170,7 +170,7 @@ class MapUiBodyState extends State { } Widget _zoomToggler() { - return FlatButton( + return TextButton( child: Text('${_zoomGesturesEnabled ? 'disable' : 'enable'} zoom'), onPressed: () { setState(() { @@ -181,7 +181,7 @@ class MapUiBodyState extends State { } Widget _zoomControlsToggler() { - return FlatButton( + return TextButton( child: Text('${_zoomControlsEnabled ? 'disable' : 'enable'} zoom controls'), onPressed: () { @@ -193,7 +193,7 @@ class MapUiBodyState extends State { } Widget _indoorViewToggler() { - return FlatButton( + return TextButton( child: Text('${_indoorViewEnabled ? 'disable' : 'enable'} indoor'), onPressed: () { setState(() { @@ -204,7 +204,7 @@ class MapUiBodyState extends State { } Widget _myLocationToggler() { - return FlatButton( + return TextButton( child: Text( '${_myLocationEnabled ? 'disable' : 'enable'} my location marker'), onPressed: () { @@ -216,7 +216,7 @@ class MapUiBodyState extends State { } Widget _myLocationButtonToggler() { - return FlatButton( + return TextButton( child: Text( '${_myLocationButtonEnabled ? 'disable' : 'enable'} my location button'), onPressed: () { @@ -228,7 +228,7 @@ class MapUiBodyState extends State { } Widget _myTrafficToggler() { - return FlatButton( + return TextButton( child: Text('${_myTrafficEnabled ? 'disable' : 'enable'} my traffic'), onPressed: () { setState(() { @@ -253,7 +253,7 @@ class MapUiBodyState extends State { if (!_isMapCreated) { return null; } - return FlatButton( + return TextButton( child: Text('${_nightMode ? 'disable' : 'enable'} night mode'), onPressed: () { if (_nightMode) { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/move_camera.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/move_camera.dart index 514a315e03db..262d362d8c3e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/move_camera.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/move_camera.dart @@ -53,7 +53,7 @@ class MoveCameraState extends State { children: [ Column( children: [ - FlatButton( + TextButton( onPressed: () { mapController.moveCamera( CameraUpdate.newCameraPosition( @@ -68,7 +68,7 @@ class MoveCameraState extends State { }, child: const Text('newCameraPosition'), ), - FlatButton( + TextButton( onPressed: () { mapController.moveCamera( CameraUpdate.newLatLng( @@ -78,7 +78,7 @@ class MoveCameraState extends State { }, child: const Text('newLatLng'), ), - FlatButton( + TextButton( onPressed: () { mapController.moveCamera( CameraUpdate.newLatLngBounds( @@ -92,7 +92,7 @@ class MoveCameraState extends State { }, child: const Text('newLatLngBounds'), ), - FlatButton( + TextButton( onPressed: () { mapController.moveCamera( CameraUpdate.newLatLngZoom( @@ -103,7 +103,7 @@ class MoveCameraState extends State { }, child: const Text('newLatLngZoom'), ), - FlatButton( + TextButton( onPressed: () { mapController.moveCamera( CameraUpdate.scrollBy(150.0, -225.0), @@ -115,7 +115,7 @@ class MoveCameraState extends State { ), Column( children: [ - FlatButton( + TextButton( onPressed: () { mapController.moveCamera( CameraUpdate.zoomBy( @@ -126,7 +126,7 @@ class MoveCameraState extends State { }, child: const Text('zoomBy with focus'), ), - FlatButton( + TextButton( onPressed: () { mapController.moveCamera( CameraUpdate.zoomBy(-0.5), @@ -134,7 +134,7 @@ class MoveCameraState extends State { }, child: const Text('zoomBy'), ), - FlatButton( + TextButton( onPressed: () { mapController.moveCamera( CameraUpdate.zoomIn(), @@ -142,7 +142,7 @@ class MoveCameraState extends State { }, child: const Text('zoomIn'), ), - FlatButton( + TextButton( onPressed: () { mapController.moveCamera( CameraUpdate.zoomOut(), @@ -150,7 +150,7 @@ class MoveCameraState extends State { }, child: const Text('zoomOut'), ), - FlatButton( + TextButton( onPressed: () { mapController.moveCamera( CameraUpdate.zoomTo(16.0), diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/padding.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/padding.dart index 94b60b7758f9..934d4c647aa4 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/padding.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/padding.dart @@ -147,7 +147,7 @@ class MarkerIconsBodyState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - FlatButton( + TextButton( child: const Text("Set Padding"), onPressed: () { setState(() { @@ -159,7 +159,7 @@ class MarkerIconsBodyState extends State { }); }, ), - FlatButton( + TextButton( child: const Text("Reset Padding"), onPressed: () { setState(() { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_circle.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_circle.dart index 954d8876d1d5..7f0447e9a279 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_circle.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_circle.dart @@ -165,15 +165,15 @@ class PlaceCircleBodyState extends State { children: [ Column( children: [ - FlatButton( + TextButton( child: const Text('add'), onPressed: _add, ), - FlatButton( + TextButton( child: const Text('remove'), onPressed: (selectedCircle == null) ? null : _remove, ), - FlatButton( + TextButton( child: const Text('toggle visible'), onPressed: (selectedCircle == null) ? null : _toggleVisible, @@ -182,19 +182,19 @@ class PlaceCircleBodyState extends State { ), Column( children: [ - FlatButton( + TextButton( child: const Text('change stroke width'), onPressed: (selectedCircle == null) ? null : _changeStrokeWidth, ), - FlatButton( + TextButton( child: const Text('change stroke color'), onPressed: (selectedCircle == null) ? null : _changeStrokeColor, ), - FlatButton( + TextButton( child: const Text('change fill color'), onPressed: (selectedCircle == null) ? null diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart index 6808e58c199e..2c5439590443 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart @@ -77,7 +77,7 @@ class PlaceMarkerBodyState extends State { builder: (BuildContext context) { return AlertDialog( actions: [ - FlatButton( + TextButton( child: const Text('OK'), onPressed: () => Navigator.of(context).pop(), ) @@ -313,19 +313,19 @@ class PlaceMarkerBodyState extends State { children: [ Column( children: [ - FlatButton( + TextButton( child: const Text('add'), onPressed: _add, ), - FlatButton( + TextButton( child: const Text('remove'), onPressed: _remove, ), - FlatButton( + TextButton( child: const Text('change info'), onPressed: _changeInfo, ), - FlatButton( + TextButton( child: const Text('change info anchor'), onPressed: _changeInfoAnchor, ), @@ -333,35 +333,35 @@ class PlaceMarkerBodyState extends State { ), Column( children: [ - FlatButton( + TextButton( child: const Text('change alpha'), onPressed: _changeAlpha, ), - FlatButton( + TextButton( child: const Text('change anchor'), onPressed: _changeAnchor, ), - FlatButton( + TextButton( child: const Text('toggle draggable'), onPressed: _toggleDraggable, ), - FlatButton( + TextButton( child: const Text('toggle flat'), onPressed: _toggleFlat, ), - FlatButton( + TextButton( child: const Text('change position'), onPressed: _changePosition, ), - FlatButton( + TextButton( child: const Text('change rotation'), onPressed: _changeRotation, ), - FlatButton( + TextButton( child: const Text('toggle visible'), onPressed: _toggleVisible, ), - FlatButton( + TextButton( child: const Text('change zIndex'), onPressed: _changeZIndex, ), @@ -371,7 +371,7 @@ class PlaceMarkerBodyState extends State { // TODO(amirh): uncomment this one the ImageStream API change makes it to stable. // https://github.com/flutter/flutter/issues/33438 // - // FlatButton( + // TextButton( // child: const Text('set marker icon'), // onPressed: () { // _getAssetIcon(context).then( diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polygon.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polygon.dart index 5713f9a099e6..af5ca16bea17 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polygon.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polygon.dart @@ -173,20 +173,20 @@ class PlacePolygonBodyState extends State { children: [ Column( children: [ - FlatButton( + TextButton( child: const Text('add'), onPressed: _add, ), - FlatButton( + TextButton( child: const Text('remove'), onPressed: (selectedPolygon == null) ? null : _remove, ), - FlatButton( + TextButton( child: const Text('toggle visible'), onPressed: (selectedPolygon == null) ? null : _toggleVisible, ), - FlatButton( + TextButton( child: const Text('toggle geodesic'), onPressed: (selectedPolygon == null) ? null @@ -196,18 +196,18 @@ class PlacePolygonBodyState extends State { ), Column( children: [ - FlatButton( + TextButton( child: const Text('change stroke width'), onPressed: (selectedPolygon == null) ? null : _changeWidth, ), - FlatButton( + TextButton( child: const Text('change stroke color'), onPressed: (selectedPolygon == null) ? null : _changeStrokeColor, ), - FlatButton( + TextButton( child: const Text('change fill color'), onPressed: (selectedPolygon == null) ? null diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polyline.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polyline.dart index 35ffd33a53c2..65201d5d1839 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polyline.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polyline.dart @@ -232,22 +232,22 @@ class PlacePolylineBodyState extends State { children: [ Column( children: [ - FlatButton( + TextButton( child: const Text('add'), onPressed: _add, ), - FlatButton( + TextButton( child: const Text('remove'), onPressed: (selectedPolyline == null) ? null : _remove, ), - FlatButton( + TextButton( child: const Text('toggle visible'), onPressed: (selectedPolyline == null) ? null : _toggleVisible, ), - FlatButton( + TextButton( child: const Text('toggle geodesic'), onPressed: (selectedPolyline == null) ? null @@ -257,29 +257,29 @@ class PlacePolylineBodyState extends State { ), Column( children: [ - FlatButton( + TextButton( child: const Text('change width'), onPressed: (selectedPolyline == null) ? null : _changeWidth, ), - FlatButton( + TextButton( child: const Text('change color'), onPressed: (selectedPolyline == null) ? null : _changeColor, ), - FlatButton( + TextButton( child: const Text('change start cap [Android only]'), onPressed: iOSorNotSelected ? null : _changeStartCap, ), - FlatButton( + TextButton( child: const Text('change end cap [Android only]'), onPressed: iOSorNotSelected ? null : _changeEndCap, ), - FlatButton( + TextButton( child: const Text('change joint type [Android only]'), onPressed: iOSorNotSelected ? null : _changeJointType, ), - FlatButton( + TextButton( child: const Text('change pattern [Android only]'), onPressed: iOSorNotSelected ? null : _changePattern, ), diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart index 872060d86039..f470a4f9783e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart @@ -47,7 +47,7 @@ class _SnapshotBodyState extends State<_SnapshotBody> { initialCameraPosition: _kInitialPosition, ), ), - FlatButton( + TextButton( child: Text('Take a snapshot'), onPressed: () async { final imageBytes = await _mapController?.takeSnapshot(); diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 06d5fb2f4257..4faadf4c2166 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter -version: 1.0.7 +version: 1.0.10 dependencies: flutter: @@ -33,4 +33,4 @@ flutter: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.22.0 <2.0.0" + flutter: ">=1.22.0" diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index 0586ac414d97..1e761681e543 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.1.0 + +* Add support for holes in Polygons. + +## 1.0.6 + +* Update Flutter SDK constraint. + ## 1.0.5 * Temporarily add a `fromJson` constructor to `BitmapDescriptor` so serialized descriptors can be synchronously re-hydrated. This will be removed when a fix for [this issue](https://github.com/flutter/flutter/issues/70330) lands. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart index 3b5e25060faf..96b39157418d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.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. +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart' show listEquals, VoidCallback; import 'package:flutter/material.dart' show Color, Colors; import 'package:meta/meta.dart' show immutable, required; @@ -46,6 +47,7 @@ class Polygon { this.fillColor = Colors.black, this.geodesic = false, this.points = const [], + this.holes = const >[], this.strokeColor = Colors.black, this.strokeWidth = 10, this.visible = true, @@ -77,6 +79,14 @@ class Polygon { /// default; to form a closed polygon, the start and end points must be the same. final List points; + /// To create an empty area within a polygon, you need to use holes. + /// To create the hole, the coordinates defining the hole path must be inside the polygon. + /// + /// The vertices of the holes to be cut out of polygon. + /// + /// Line segments of each points of hole are drawn inside polygon between consecutive hole points. + final List> holes; + /// True if the marker is visible. final bool visible; @@ -106,6 +116,7 @@ class Polygon { Color fillColorParam, bool geodesicParam, List pointsParam, + List> holesParam, Color strokeColorParam, int strokeWidthParam, bool visibleParam, @@ -118,6 +129,7 @@ class Polygon { fillColor: fillColorParam ?? fillColor, geodesic: geodesicParam ?? geodesic, points: pointsParam ?? points, + holes: holesParam ?? holes, strokeColor: strokeColorParam ?? strokeColor, strokeWidth: strokeWidthParam ?? strokeWidth, visible: visibleParam ?? visible, @@ -154,6 +166,10 @@ class Polygon { json['points'] = _pointsToJson(); } + if (holes != null) { + json['holes'] = _holesToJson(); + } + return json; } @@ -167,6 +183,7 @@ class Polygon { fillColor == typedOther.fillColor && geodesic == typedOther.geodesic && listEquals(points, typedOther.points) && + DeepCollectionEquality().equals(holes, typedOther.holes) && visible == typedOther.visible && strokeColor == typedOther.strokeColor && strokeWidth == typedOther.strokeWidth && @@ -183,4 +200,16 @@ class Polygon { } return result; } + + List> _holesToJson() { + final List> result = >[]; + for (final List hole in holes) { + final List jsonHole = []; + for (final LatLng point in hole) { + jsonHole.add(point.toJson()); + } + result.add(jsonHole); + } + return result; + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index a2b5ff56fee1..d8b260a7a3eb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the google_maps_flutter plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_platform_interface # 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: 1.0.5 +version: 1.1.0 dependencies: flutter: @@ -11,6 +11,7 @@ dependencies: meta: ^1.0.5 plugin_platform_interface: ^1.0.1 stream_transform: ^1.2.0 + collection: ^1.14.13 dev_dependencies: flutter_test: @@ -20,4 +21,4 @@ dev_dependencies: environment: sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + flutter: ">=1.9.1+hotfix.4" diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index dcafa12a2c13..2d03ab254bc0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.0+9 + +* Update Flutter SDK constraint. + ## 0.1.0+8 * Update `package:google_maps_flutter_platform_interface` to `^1.0.5`. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml index 237415318aac..b41e24c25357 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -1,7 +1,7 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter -version: 0.1.0+8 +version: 0.1.0+9 flutter: plugin: @@ -33,4 +33,4 @@ dev_dependencies: environment: sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + flutter: ">=1.10.0" diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md index 3425ff6c34b0..4afb1a0e98bf 100644 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md +++ b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md @@ -1,3 +1,15 @@ +## 1.0.4 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 1.0.3 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 1.0.2 + +* Update Flutter SDK constraint. + ## 1.0.1 * Update android compileSdkVersion to 29. diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/README.md b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/README.md index 689071dcfe8d..a7ad235b71d5 100755 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/README.md +++ b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the google_sign_in plugin with the `googleapis` package. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart index a238ca3bf8b5..597ab563ae5b 100755 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart +++ b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart @@ -111,11 +111,11 @@ class SignInDemoState extends State { ), const Text('Signed in successfully.'), Text(_contactText ?? ''), - RaisedButton( + ElevatedButton( child: const Text('SIGN OUT'), onPressed: _handleSignOut, ), - RaisedButton( + ElevatedButton( child: const Text('REFRESH'), onPressed: _handleGetContact, ), @@ -126,7 +126,7 @@ class SignInDemoState extends State { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ const Text('You are not currently signed in.'), - RaisedButton( + ElevatedButton( child: const Text('SIGN IN'), onPressed: _handleSignIn, ), diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/pubspec.yaml b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/pubspec.yaml index c978ce446714..9da5f0baa848 100644 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/pubspec.yaml +++ b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/pubspec.yaml @@ -6,7 +6,7 @@ name: extension_google_sign_in_as_googleapis_auth description: A bridge package between google_sign_in and googleapis_auth, to create Authenticated Clients from google_sign_in user credentials. -version: 1.0.1 +version: 1.0.4 homepage: https://github.com/flutter/plugins/google_sign_in/extension_google_sign_in_as_googleapis_auth dependencies: @@ -25,4 +25,4 @@ dev_dependencies: environment: sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + flutter: ">=1.12.13+hotfix.4" diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 7f4bfddfa45a..7f5b4f2bdd17 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,15 @@ +## 4.5.9 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 4.5.8 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 4.5.7 + +* Update Flutter SDK constraint. + ## 4.5.6 * Fix deprecated member warning in tests. diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index fc877171c4d2..61c4380cdcb7 100755 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -1,6 +1,6 @@ # google_sign_in -[![pub package](https://img.shields.io/pub/v/google_sign_in.svg)](https://pub.dartlang.org/packages/google_sign_in) +[![pub package](https://img.shields.io/pub/v/google_sign_in.svg)](https://pub.dev/packages/google_sign_in) A Flutter plugin for [Google Sign In](https://developers.google.com/identity/). @@ -63,7 +63,7 @@ plugin could be an option. ## Usage ### Import the package -To use this plugin, follow the [plugin installation instructions](https://pub.dartlang.org/packages/google_sign_in#pub-pkg-tab-installing). +To use this plugin, follow the [plugin installation instructions](https://pub.dev/packages/google_sign_in#pub-pkg-tab-installing). ### Use the plugin Add the following import to your Dart code: diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index 6c66d56085db..a738c248a4a4 100755 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -120,11 +120,11 @@ class SignInDemoState extends State { ), const Text("Signed in successfully."), Text(_contactText ?? ''), - RaisedButton( + ElevatedButton( child: const Text('SIGN OUT'), onPressed: _handleSignOut, ), - RaisedButton( + ElevatedButton( child: const Text('REFRESH'), onPressed: _handleGetContact, ), @@ -135,7 +135,7 @@ class SignInDemoState extends State { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ const Text("You are not currently signed in."), - RaisedButton( + ElevatedButton( child: const Text('SIGN IN'), onPressed: _handleSignIn, ), diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index a63091b945b2..6e0366c790bf 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in -version: 4.5.6 +version: 4.5.9 flutter: plugin: @@ -39,4 +39,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + flutter: ">=1.12.13+hotfix.4" diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index bc709197d77d..7305800296f9 100755 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 +// @dart = 2.9 import 'dart:async'; diff --git a/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md index aa8ad2cff80f..e69e912195bf 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.3 + +* Update Flutter SDK constraint. + ## 1.1.2 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml index 7bc63d84110c..8edeba0072a8 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the google_sign_in plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_platform_interface # 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: 1.1.2 +version: 1.1.3 dependencies: flutter: @@ -19,4 +19,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + flutter: ">=1.10.0" diff --git a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md index d71badc53bfc..d1353f723fd5 100644 --- a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.2+1 + +* Update Flutter SDK constraint. + ## 0.9.2 * Throw PlatformExceptions from where the GMaps SDK may throw exceptions: `init()` and `signIn()`. diff --git a/packages/google_sign_in/google_sign_in_web/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/pubspec.yaml index 70758ac6830d..ac9d36bd15be 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in_web description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web. homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_web -version: 0.9.2 +version: 0.9.2+1 flutter: plugin: @@ -31,4 +31,4 @@ dev_dependencies: environment: sdk: ">=2.6.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + flutter: ">=1.12.13+hotfix.4" diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 26e8b100cd20..1b3146d532fa 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,33 @@ +## 0.6.7+21 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.6.7+20 + +* Updated README.md to show the new Android API requirements. + +## 0.6.7+19 + +* Do not copy static field to another static field. + +## 0.6.7+18 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.6.7+17 + +* iOS: fix `User-facing text should use localized string macro` warning. + +## 0.6.7+16 + +* Update Flutter SDK constraint. + +## 0.6.7+15 + +* Fix element type in XCUITests to look for staticText type when searching for texts. + * See https://github.com/flutter/flutter/issues/71927 +* Minor update in XCUITests to search for different elements on iOS 14 and above. + ## 0.6.7+14 * Set up XCUITests. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 2e062a024597..ca8ad763c553 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -1,13 +1,13 @@ # Image Picker plugin for Flutter -[![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dartlang.org/packages/image_picker) +[![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dev/packages/image_picker) A Flutter plugin for iOS and Android for picking images from the image library, and taking new pictures with the camera. ## Installation -First, add `image_picker` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +First, add `image_picker` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). ### iOS @@ -19,10 +19,10 @@ Add the following keys to your _Info.plist_ file, located in `/ios ### Android -#### API 29+ +#### API < 29 No configuration required - the plugin should work out of the box. -#### API < 29 +#### API 29+ Add `android:requestLegacyExternalStorage="true"` as an attribute to the `` tag in AndroidManifest.xml. The [attribute](https://developer.android.com/training/data-storage/compatibility) is `false` by default on apps targeting Android Q. diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m b/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m index 1ba8457c9709..74df795a3df3 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m +++ b/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m @@ -21,18 +21,33 @@ - (void)setUp { self.continueAfterFailure = NO; self.app = [[XCUIApplication alloc] init]; [self.app launch]; + __weak typeof(self) weakSelf = self; [self addUIInterruptionMonitorWithDescription:@"Permission popups" handler:^BOOL(XCUIElement* _Nonnull interruptingElement) { - XCUIElement* ok = interruptingElement.buttons[@"OK"]; - if (ok.exists) { - [ok tap]; - } - // iOS 14. - XCUIElement* allPhotoPermission = - interruptingElement - .buttons[@"Allow Access to All Photos"]; - if (allPhotoPermission.exists) { + if (@available(iOS 14, *)) { + XCUIElement* allPhotoPermission = + interruptingElement + .buttons[@"Allow Access to All Photos"]; + if (![allPhotoPermission waitForExistenceWithTimeout: + kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", + self.app.debugDescription); + XCTFail(@"Failed due to not able to find " + @"allPhotoPermission button with %@ seconds", + @(kElementWaitingTime)); + } [allPhotoPermission tap]; + } else { + XCUIElement* ok = interruptingElement.buttons[@"OK"]; + if (![ok waitForExistenceWithTimeout: + kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", + self.app.debugDescription); + XCTFail(@"Failed due to not able to find ok button " + @"with %@ seconds", + @(kElementWaitingTime)); + } + [ok tap]; } return YES; }]; @@ -92,10 +107,21 @@ - (void)launchPickerAndCancel { [cancelButton tap]; // Find the "not picked image text". - XCUIElement* imageNotPickedText = [self.app.otherElements + XCUIElement* imageNotPickedText = [self.app.staticTexts elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label == %@", @"You have not yet picked an image."]]; + if (![imageNotPickedText waitForExistenceWithTimeout:kElementWaitingTime]) { + // Before https://github.com/flutter/engine/pull/22811 the label's a11y type was otherElements. + // TODO(cyanglaz): Remove this after + // https://github.com/flutter/flutter/commit/057e8230743ec96f33b73948ccd6b80081e3615e rolled to + // stable. + // https://github.com/flutter/flutter/issues/71927 + imageNotPickedText = [self.app.otherElements + elementMatchingPredicate:[NSPredicate + predicateWithFormat:@"label == %@", + @"You have not yet picked an image."]]; + } if (![imageNotPickedText waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find imageNotPickedText with %@ seconds", diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 3047ac13f235..73327ef0caa6 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -327,13 +327,13 @@ class _MyHomePageState extends State { ], ), actions: [ - FlatButton( + TextButton( child: const Text('CANCEL'), onPressed: () { Navigator.of(context).pop(); }, ), - FlatButton( + TextButton( child: const Text('PICK'), onPressed: () { double width = maxWidthController.text.isNotEmpty diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m index 00fdec245aaf..8d260f31b055 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m @@ -146,10 +146,10 @@ - (void)showCamera { animated:YES completion:nil]; } else { - [[[UIAlertView alloc] initWithTitle:@"Error" - message:@"Camera not available." + [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Error", nil) + message:NSLocalizedString(@"Camera not available.", nil) delegate:nil - cancelButtonTitle:@"OK" + cancelButtonTitle:NSLocalizedString(@"OK", nil) otherButtonTitles:nil] show]; self.result(nil); self.result = nil; diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 82c199e007f4..5c0683c3f29f 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 @@ export 'package:image_picker_platform_interface/image_picker_platform_interface. class ImagePicker { /// The platform interface that drives this plugin @visibleForTesting - static ImagePickerPlatform platform = ImagePickerPlatform.instance; + static ImagePickerPlatform get platform => ImagePickerPlatform.instance; /// Returns a [File] object pointing to the image that was picked. /// diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 2dcd7c137b7d..789ca13d5bcb 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker -version: 0.6.7+14 +version: 0.6.7+21 flutter: plugin: @@ -25,8 +25,10 @@ dev_dependencies: sdk: flutter integration_test: path: ../../integration_test + mockito: ^4.1.3 pedantic: ^1.8.0 + plugin_platform_interface: ^1.0.3 environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + flutter: ">=1.10.0" 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 0ada1b261363..7172975ded5d 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -5,6 +5,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -26,6 +29,15 @@ void main() { log.clear(); }); + test('ImagePicker platform instance overrides the actual platform used', + () { + final ImagePickerPlatform savedPlatform = ImagePickerPlatform.instance; + final MockPlatform mockPlatform = MockPlatform(); + ImagePickerPlatform.instance = mockPlatform; + expect(ImagePicker.platform, mockPlatform); + ImagePickerPlatform.instance = savedPlatform; + }); + group('#pickImage', () { test('passes the image source argument correctly', () async { await picker.getImage(source: ImageSource.camera); @@ -336,3 +348,7 @@ void main() { }); }); } + +class MockPlatform extends Mock + with MockPlatformInterfaceMixin + implements ImagePickerPlatform {} diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index 604314240a1e..4c452ee78de9 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.0+3 + +* Update Flutter SDK constraint. + # 0.1.0+2 * Adds Video MIME Types for the safari browser for acception diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 32e89437415e..b7e079b39ce0 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/i # 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.1.0+2 +version: 0.1.0+3 flutter: plugin: @@ -29,4 +29,4 @@ dev_dependencies: environment: sdk: ">=2.5.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + flutter: ">=1.10.0" diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 1ed45028f513..efcef0146cdc 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 @@ +## 1.1.5 + +* Update Flutter SDK constraint. + +## 1.1.4 + +* Pass `Uri`s to `package:http` methods, instead of strings, in preparation for a major version update in `http`. + ## 1.1.3 * Update documentation of `pickImage()` regarding HEIC images. diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart index 0faf531f3f75..ee5145009dc7 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart @@ -24,7 +24,7 @@ class PickedFile extends PickedFileBase { if (_initBytes != null) { return Future.value(UnmodifiableUint8ListView(_initBytes)); } - return http.readBytes(path); + return http.readBytes(Uri.parse(path)); } @override diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 39a65284e247..7943a2a3eccd 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the image_picker plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_platform_interface # 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: 1.1.3 +version: 1.1.5 dependencies: flutter: @@ -20,4 +20,4 @@ dev_dependencies: environment: sdk: ">=2.5.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + flutter: ">=1.10.0" diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index 6207da9b403c..3c77e0c313f5 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,3 +1,19 @@ +## 0.3.5+1 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.3.5 + +* [Android] Fixed: added support for the SERVICE_TIMEOUT (-3) response code. + +## 0.3.4+18 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.3.4+17 + +* Update Flutter SDK constraint. + ## 0.3.4+16 * Add Dartdocs to all public APIs. diff --git a/packages/in_app_purchase/README.md b/packages/in_app_purchase/README.md index 021811842b2f..431f2810c165 100644 --- a/packages/in_app_purchase/README.md +++ b/packages/in_app_purchase/README.md @@ -181,7 +181,7 @@ WARNING! Failure to call `InAppPurchaseConnection.completePurchase` and get a su ## Development This plugin uses -[json_serializable](https://pub.dartlang.org/packages/json_serializable) for the +[json_serializable](https://pub.dev/packages/json_serializable) for the many data structs passed between the underlying platform layers and Dart. After editing any of the serialized data structs, rebuild the serializers by running `flutter packages pub run build_runner build --delete-conflicting-outputs`. diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart index b285b6e2bb87..911edae98cfb 100644 --- a/packages/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/example/lib/main.dart @@ -245,10 +245,12 @@ class _MyAppState extends State<_MyApp> { ), trailing: previousPurchase != null ? Icon(Icons.check) - : FlatButton( + : TextButton( child: Text(productDetails.price), - color: Colors.green[800], - textColor: Colors.white, + style: TextButton.styleFrom( + backgroundColor: Colors.green[800], + primary: Colors.white, + ), onPressed: () { PurchaseParam purchaseParam = PurchaseParam( productDetails: productDetails, diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart index 0c8b348f4cd0..2aa91d9f9225 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart @@ -314,6 +314,11 @@ enum BillingResponse { // WARNING: Changes to this class need to be reflected in our generated code. // Run `flutter packages pub run build_runner watch` to rebuild and watch for // further changes. + + /// The request has reached the maximum timeout before Google Play responds. + @JsonValue(-3) + serviceTimeout, + /// The requested feature is not supported by Play Store on the current device. @JsonValue(-2) featureNotSupported, diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart index 899304b08273..947700df64df 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart @@ -43,6 +43,7 @@ T _$enumDecode( } const _$BillingResponseEnumMap = { + BillingResponse.serviceTimeout: -3, BillingResponse.featureNotSupported: -2, BillingResponse.serviceDisconnected: -1, BillingResponse.ok: 0, diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml index eda865e487f7..02240ea654db 100644 --- a/packages/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/pubspec.yaml @@ -1,7 +1,7 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase -version: 0.3.4+16 +version: 0.3.5+1 dependencies: async: ^2.0.8 @@ -37,4 +37,4 @@ flutter: environment: sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart index 54f7c3eda77f..eee33a698237 100644 --- a/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -39,6 +39,24 @@ void main() { }); }); + // Make sure that the enum values are supported and that the converter call + // does not fail + test('response states', () async { + BillingResponseConverter converter = BillingResponseConverter(); + converter.fromJson(-3); + converter.fromJson(-2); + converter.fromJson(-1); + converter.fromJson(0); + converter.fromJson(1); + converter.fromJson(2); + converter.fromJson(3); + converter.fromJson(4); + converter.fromJson(5); + converter.fromJson(6); + converter.fromJson(7); + converter.fromJson(8); + }); + group('startConnection', () { final String methodName = 'BillingClient#startConnection(BillingClientStateListener)'; diff --git a/packages/integration_test/CHANGELOG.md b/packages/integration_test/CHANGELOG.md index 534d7063db6c..4bbfe591d6cc 100644 --- a/packages/integration_test/CHANGELOG.md +++ b/packages/integration_test/CHANGELOG.md @@ -1,3 +1,19 @@ +## 1.0.2 + +* Update Flutter SDK constraint. + +## 1.0.1 + +* Remove usages of deprecated `List` constructor. + +## 1.0.0 + +* Public stable release of plugin. + +## 0.9.3 + +* Update README to mention that only `testWidgets` is supported for declaring tests. + ## 0.9.2+2 * Broaden the constraint on vm_service. diff --git a/packages/integration_test/README.md b/packages/integration_test/README.md index e94cb1e01854..676041eefae5 100644 --- a/packages/integration_test/README.md +++ b/packages/integration_test/README.md @@ -14,6 +14,8 @@ Create a `integration_test/` directory for your package. In this directory, create a `_test.dart`, using the following as a starting point to make assertions. +Note: You should only use `testWidgets` to declare your tests, or errors will not be reported correctly. + ```dart import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/integration_test/lib/common.dart b/packages/integration_test/lib/common.dart index 53714a8e97ee..da93b3b51450 100644 --- a/packages/integration_test/lib/common.dart +++ b/packages/integration_test/lib/common.dart @@ -86,7 +86,7 @@ class Response { /// Create a list of Strings from [_failureDetails]. List _failureDetailsAsString() { - final List list = List(); + final List list = []; if (_failureDetails == null || _failureDetails.isEmpty) { return list; } @@ -100,7 +100,7 @@ class Response { /// Creates a [Failure] list using a json response. static List _failureDetailsFromJson(List list) { - final List failureList = List(); + final List failureList = []; list.forEach((s) { final String failure = s as String; failureList.add(Failure.fromJsonString(failure)); diff --git a/packages/integration_test/pubspec.yaml b/packages/integration_test/pubspec.yaml index 337fc55b11bf..997faa79e1ca 100644 --- a/packages/integration_test/pubspec.yaml +++ b/packages/integration_test/pubspec.yaml @@ -1,11 +1,11 @@ name: integration_test description: Runs tests that use the flutter_test API as integration tests. -version: 0.9.2+2 +version: 1.0.2 homepage: https://github.com/flutter/plugins/tree/master/packages/integration_test environment: sdk: ">=2.2.2 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + flutter: ">=1.12.13+hotfix.5" dependencies: flutter: diff --git a/packages/ios_platform_images/CHANGELOG.md b/packages/ios_platform_images/CHANGELOG.md index a83f22ec7571..4b11b40f5510 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.2+4 + +* Update Flutter SDK constraint. + ## 0.1.2+3 * Remove no-op android folder in the example app. diff --git a/packages/ios_platform_images/pubspec.yaml b/packages/ios_platform_images/pubspec.yaml index 482097a515b8..7049b62cf00a 100644 --- a/packages/ios_platform_images/pubspec.yaml +++ b/packages/ios_platform_images/pubspec.yaml @@ -1,11 +1,11 @@ name: ios_platform_images description: A plugin to share images between Flutter and iOS in add-to-app setups. -version: 0.1.2+3 +version: 0.1.2+4 homepage: https://github.com/flutter/plugins/tree/master/packages/ios_platform_images/ios_platform_images environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + flutter: ">=1.12.13+hotfix.5" dependencies: flutter: @@ -29,7 +29,6 @@ flutter: platforms: ios: pluginClass: IosPlatformImagesPlugin - # To add assets to your plugin package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg @@ -40,7 +39,6 @@ flutter: # # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. - # To add custom fonts to your plugin package, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md index 96388a76ab4f..b27ec83d41a7 100644 --- a/packages/local_auth/CHANGELOG.md +++ b/packages/local_auth/CHANGELOG.md @@ -1,3 +1,23 @@ +## 1.0.0-nullsafety.3 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 1.0.0-nullsafety.2 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 1.0.0-nullsafety.1 + +* Update README for Android Integration. + +## 1.0.0-nullsafety + +* Migrate to null safety. + +## 0.6.3+5 + +* Update Flutter SDK constraint. + ## 0.6.3+4 * Update Dart SDK constraint in example. @@ -174,4 +194,4 @@ ## 0.0.1 -* Initial release of local authentication plugin. +* Initial release of local authentication plugin. \ No newline at end of file diff --git a/packages/local_auth/README.md b/packages/local_auth/README.md index ca2aa49bed23..516561be4230 100644 --- a/packages/local_auth/README.md +++ b/packages/local_auth/README.md @@ -142,6 +142,42 @@ opposed to Activity. This can be easily done by switching to use `FlutterFragmentActivity` as opposed to `FlutterActivity` in your manifest (or your own Activity class if you are extending the base class). +Update your MainActivity.java: + +```java +import android.os.Bundle; +import io.flutter.app.FlutterFragmentActivity; +import io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin; +import io.flutter.plugins.localauth.LocalAuthPlugin; + +public class MainActivity extends FlutterFragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + FlutterAndroidLifecyclePlugin.registerWith( + registrarFor( + "io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin")); + LocalAuthPlugin.registerWith(registrarFor("io.flutter.plugins.localauth.LocalAuthPlugin")); + } +} +``` + +OR + +Update your MainActivity.kt: + +```kotlin +import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugins.GeneratedPluginRegistrant + +class MainActivity: FlutterFragmentActivity() { + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine) + } +} +``` + Update your project's `AndroidManifest.xml` file to include the `USE_FINGERPRINT` permissions: @@ -170,6 +206,6 @@ app resumes. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/MainActivityTest.java b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java similarity index 100% rename from packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/MainActivityTest.java rename to packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java diff --git a/packages/local_auth/example/lib/main.dart b/packages/local_auth/example/lib/main.dart index 06e33b9853be..0a07e2c4437d 100644 --- a/packages/local_auth/example/lib/main.dart +++ b/packages/local_auth/example/lib/main.dart @@ -21,16 +21,17 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { final LocalAuthentication auth = LocalAuthentication(); - bool _canCheckBiometrics; - List _availableBiometrics; + late bool _canCheckBiometrics; + late List _availableBiometrics; String _authorized = 'Not Authorized'; bool _isAuthenticating = false; Future _checkBiometrics() async { - bool canCheckBiometrics; + late bool canCheckBiometrics; try { canCheckBiometrics = await auth.canCheckBiometrics; } on PlatformException catch (e) { + canCheckBiometrics = false; print(e); } if (!mounted) return; @@ -41,10 +42,11 @@ class _MyAppState extends State { } Future _getAvailableBiometrics() async { - List availableBiometrics; + late List availableBiometrics; try { availableBiometrics = await auth.getAvailableBiometrics(); } on PlatformException catch (e) { + availableBiometrics = []; print(e); } if (!mounted) return; @@ -97,17 +99,17 @@ class _MyAppState extends State { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Text('Can check biometrics: $_canCheckBiometrics\n'), - RaisedButton( + ElevatedButton( child: const Text('Check biometrics'), onPressed: _checkBiometrics, ), Text('Available biometrics: $_availableBiometrics\n'), - RaisedButton( + ElevatedButton( child: const Text('Get available biometrics'), onPressed: _getAvailableBiometrics, ), Text('Current State: $_authorized\n'), - RaisedButton( + ElevatedButton( child: Text(_isAuthenticating ? 'Cancel' : 'Authenticate'), onPressed: _isAuthenticating ? _cancelAuthentication : _authenticate, diff --git a/packages/local_auth/example/pubspec.yaml b/packages/local_auth/example/pubspec.yaml index 0861c51adbc6..9c7b91e672e6 100644 --- a/packages/local_auth/example/pubspec.yaml +++ b/packages/local_auth/example/pubspec.yaml @@ -18,5 +18,5 @@ flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0-0 <3.0.0" flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/packages/local_auth/integration_test/local_auth_test.dart b/packages/local_auth/integration_test/local_auth_test.dart index 47e5dfa67912..c09810032461 100644 --- a/packages/local_auth/integration_test/local_auth_test.dart +++ b/packages/local_auth/integration_test/local_auth_test.dart @@ -1,3 +1,9 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.9 + import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/local_auth/lib/auth_strings.dart b/packages/local_auth/lib/auth_strings.dart index a8f34f88723c..3afc23827d98 100644 --- a/packages/local_auth/lib/auth_strings.dart +++ b/packages/local_auth/lib/auth_strings.dart @@ -25,14 +25,14 @@ class AndroidAuthMessages { this.goToSettingsDescription, }); - final String fingerprintHint; - final String fingerprintNotRecognized; - final String fingerprintSuccess; - final String cancelButton; - final String signInTitle; - final String fingerprintRequiredTitle; - final String goToSettingsButton; - final String goToSettingsDescription; + final String? fingerprintHint; + final String? fingerprintNotRecognized; + final String? fingerprintSuccess; + final String? cancelButton; + final String? signInTitle; + final String? fingerprintRequiredTitle; + final String? goToSettingsButton; + final String? goToSettingsDescription; Map get args { return { @@ -62,10 +62,10 @@ class IOSAuthMessages { this.cancelButton, }); - final String lockOut; - final String goToSettingsButton; - final String goToSettingsDescription; - final String cancelButton; + final String? lockOut; + final String? goToSettingsButton; + final String? goToSettingsDescription; + final String? cancelButton; Map get args { return { diff --git a/packages/local_auth/lib/local_auth.dart b/packages/local_auth/lib/local_auth.dart index b2b03b920d64..f1dbdd4840a8 100644 --- a/packages/local_auth/lib/local_auth.dart +++ b/packages/local_auth/lib/local_auth.dart @@ -67,7 +67,7 @@ class LocalAuthentication { /// [PlatformException] with error code [otherOperatingSystem] on the iOS /// simulator. Future authenticateWithBiometrics({ - @required String localizedReason, + required String localizedReason, bool useErrorDialogs = true, bool stickyAuth = false, AndroidAuthMessages androidAuthStrings = const AndroidAuthMessages(), @@ -92,8 +92,9 @@ class LocalAuthentication { 'operating systems.', details: 'Your operating system is ${_platform.operatingSystem}'); } - return await _channel.invokeMethod( - 'authenticateWithBiometrics', args); + final bool? result = + await _channel.invokeMethod('authenticateWithBiometrics', args); + return result!; } /// Returns true if auth was cancelled successfully. @@ -101,18 +102,20 @@ class LocalAuthentication { /// Returns false if there was some error or no auth in progress. /// /// Returns [Future] bool true or false: - Future stopAuthentication() { + Future stopAuthentication() async { if (_platform.isAndroid) { - return _channel.invokeMethod('stopAuthentication'); + final bool? result = + await _channel.invokeMethod('stopAuthentication'); + return result!; } - return Future.sync(() => true); + return true; } /// Returns true if device is capable of checking biometrics /// /// Returns a [Future] bool true or false: Future get canCheckBiometrics async => - (await _channel.invokeListMethod('getAvailableBiometrics')) + (await _channel.invokeListMethod('getAvailableBiometrics'))! .isNotEmpty; /// Returns a list of enrolled biometrics @@ -122,10 +125,10 @@ class LocalAuthentication { /// - BiometricType.fingerprint /// - BiometricType.iris (not yet implemented) Future> getAvailableBiometrics() async { - final List result = - (await _channel.invokeListMethod('getAvailableBiometrics')); + final List? result = + await _channel.invokeListMethod('getAvailableBiometrics'); final List biometrics = []; - result.forEach((String value) { + result!.forEach((String value) { switch (value) { case 'face': biometrics.add(BiometricType.face); diff --git a/packages/local_auth/pubspec.yaml b/packages/local_auth/pubspec.yaml index 6ba77aca6679..050cedb5d7d0 100644 --- a/packages/local_auth/pubspec.yaml +++ b/packages/local_auth/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth description: Flutter plugin for Android and iOS device authentication sensors such as Fingerprint Reader and Touch ID. homepage: https://github.com/flutter/plugins/tree/master/packages/local_auth -version: 0.6.3+4 +version: 1.0.0-nullsafety.3 flutter: plugin: @@ -16,10 +16,10 @@ flutter: dependencies: flutter: sdk: flutter - meta: ^1.0.5 - intl: ">=0.15.1 <0.17.0" - platform: ">=2.0.0 <4.0.0" - flutter_plugin_android_lifecycle: ^1.0.2 + meta: ^1.3.0-nullsafety.3 + intl: ^0.17.0-nullsafety.2 + platform: ^3.0.0-nullsafety.4 + flutter_plugin_android_lifecycle: ^2.0.0-nullsafety dev_dependencies: integration_test: @@ -28,8 +28,8 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/local_auth/test/local_auth_test.dart b/packages/local_auth/test/local_auth_test.dart index 205c5f785708..52b8dbf21f72 100644 --- a/packages/local_auth/test/local_auth_test.dart +++ b/packages/local_auth/test/local_auth_test.dart @@ -19,7 +19,7 @@ void main() { ); final List log = []; - LocalAuthentication localAuthentication; + late LocalAuthentication localAuthentication; setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) { diff --git a/packages/package_info/CHANGELOG.md b/packages/package_info/CHANGELOG.md index 21319fe4fbf1..ebb95c1da17e 100644 --- a/packages/package_info/CHANGELOG.md +++ b/packages/package_info/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.4.3+3 + +* Update Flutter SDK constraint. + ## 0.4.3+2 * Remove unused `test` dependency. diff --git a/packages/package_info/pubspec.yaml b/packages/package_info/pubspec.yaml index cb46ca66b9a4..884a71659a48 100644 --- a/packages/package_info/pubspec.yaml +++ b/packages/package_info/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/package_info # 0.4.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.4.3+2 +version: 0.4.3+3 flutter: plugin: @@ -33,4 +33,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index 646a2e36bc9e..bd6c0bc651f5 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,15 @@ +## 1.6.27 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 1.6.26 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 1.6.25 + +* Update Flutter SDK constraint. + ## 1.6.24 * Remove unused `test` dependency. diff --git a/packages/path_provider/path_provider/README.md b/packages/path_provider/path_provider/README.md index e8d97e0106a3..0c80a5b34075 100644 --- a/packages/path_provider/path_provider/README.md +++ b/packages/path_provider/path_provider/README.md @@ -1,13 +1,13 @@ # path_provider -[![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dartlang.org/packages/path_provider) +[![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dev/packages/path_provider) A Flutter plugin for finding commonly used locations on the filesystem. Supports iOS, Android, Linux and MacOS. Not all methods are supported on all platforms. ## Usage -To use this plugin, add `path_provider` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add `path_provider` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). ### Example diff --git a/packages/path_provider/path_provider/example/lib/main.dart b/packages/path_provider/path_provider/example/lib/main.dart index ce496e9d4e63..8e929a6882fe 100644 --- a/packages/path_provider/path_provider/example/lib/main.dart +++ b/packages/path_provider/path_provider/example/lib/main.dart @@ -129,7 +129,7 @@ class _MyHomePageState extends State { children: [ Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Temporary Directory'), onPressed: _requestTempDirectory, ), @@ -138,7 +138,7 @@ class _MyHomePageState extends State { future: _tempDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Application Documents Directory'), onPressed: _requestAppDocumentsDirectory, ), @@ -147,7 +147,7 @@ class _MyHomePageState extends State { future: _appDocumentsDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Application Support Directory'), onPressed: _requestAppSupportDirectory, ), @@ -156,7 +156,7 @@ class _MyHomePageState extends State { future: _appSupportDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Application Library Directory'), onPressed: _requestAppLibraryDirectory, ), @@ -165,7 +165,7 @@ class _MyHomePageState extends State { future: _appLibraryDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: Text( '${Platform.isIOS ? "External directories are unavailable " "on iOS" : "Get External Storage Directory"}'), onPressed: @@ -177,7 +177,7 @@ class _MyHomePageState extends State { Column(children: [ Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: Text( '${Platform.isIOS ? "External directories are unavailable " "on iOS" : "Get External Storage Directories"}'), onPressed: Platform.isIOS @@ -196,7 +196,7 @@ class _MyHomePageState extends State { Column(children: [ Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: Text( '${Platform.isIOS ? "External directories are unavailable " "on iOS" : "Get External Cache Directories"}'), onPressed: diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index b3960941acff..649b3420d72f 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -1,7 +1,7 @@ name: path_provider description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider -version: 1.6.24 +version: 1.6.27 flutter: plugin: @@ -40,4 +40,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index bf043c6e6954..ee382b04710b 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.1+3 + +* Update Flutter SDK constraint. + ## 0.1.1+2 * Log errors in the example when calls to the `path_provider` fail. diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index fac82d24829f..adabbdd45246 100644 --- a/packages/path_provider/path_provider_linux/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -1,6 +1,6 @@ name: path_provider_linux description: linux implementation of the path_provider plugin -version: 0.1.1+2 +version: 0.1.1+3 homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_linux flutter: @@ -12,7 +12,7 @@ flutter: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + flutter: ">=1.10.0" dependencies: path: ^1.6.4 diff --git a/packages/path_provider/path_provider_macos/CHANGELOG.md b/packages/path_provider/path_provider_macos/CHANGELOG.md index ba2a38e563d6..b082aefd9da6 100644 --- a/packages/path_provider/path_provider_macos/CHANGELOG.md +++ b/packages/path_provider/path_provider_macos/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.0.4+8 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.0.4+7 + +* Update Flutter SDK constraint. + ## 0.0.4+6 * Remove unused `test` dependency. diff --git a/packages/path_provider/path_provider_macos/example/lib/main.dart b/packages/path_provider/path_provider_macos/example/lib/main.dart index 473a989914f6..1c1c90b983c3 100644 --- a/packages/path_provider/path_provider_macos/example/lib/main.dart +++ b/packages/path_provider/path_provider_macos/example/lib/main.dart @@ -98,7 +98,7 @@ class _MyHomePageState extends State { children: [ Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Temporary Directory'), onPressed: _requestTempDirectory, ), @@ -107,7 +107,7 @@ class _MyHomePageState extends State { future: _tempDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Application Documents Directory'), onPressed: _requestAppDocumentsDirectory, ), @@ -116,7 +116,7 @@ class _MyHomePageState extends State { future: _appDocumentsDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Application Support Directory'), onPressed: _requestAppSupportDirectory, ), @@ -125,7 +125,7 @@ class _MyHomePageState extends State { future: _appSupportDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Application Library Directory'), onPressed: _requestAppLibraryDirectory, ), @@ -134,7 +134,7 @@ class _MyHomePageState extends State { future: _appLibraryDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Downlads Directory'), onPressed: _requestDownloadsDirectory, ), diff --git a/packages/path_provider/path_provider_macos/pubspec.yaml b/packages/path_provider/path_provider_macos/pubspec.yaml index 69241491d29d..0af1cfbf0aaa 100644 --- a/packages/path_provider/path_provider_macos/pubspec.yaml +++ b/packages/path_provider/path_provider_macos/pubspec.yaml @@ -3,7 +3,7 @@ description: macOS implementation of the path_provider plugin # 0.0.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.0.4+6 +version: 0.0.4+8 homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_macos flutter: @@ -14,7 +14,7 @@ flutter: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + flutter: ">=1.10.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md index 744764c3215a..97121268c790 100644 --- a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md +++ b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.0-nullsafety + +* Migrate to null safety. + +## 1.0.5 + +* Update Flutter SDK constraint. + ## 1.0.4 * Remove unused `test` dependency. diff --git a/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart b/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart index 4f796aaeec33..1ff2a978c5a4 100644 --- a/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart +++ b/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart @@ -40,26 +40,26 @@ abstract class PathProviderPlatform extends PlatformInterface { /// Path to the temporary directory on the device that is not backed up and is /// suitable for storing caches of downloaded files. - Future getTemporaryPath() { + Future getTemporaryPath() { throw UnimplementedError('getTemporaryPath() has not been implemented.'); } /// Path to a directory where the application may place application support /// files. - Future getApplicationSupportPath() { + Future getApplicationSupportPath() { throw UnimplementedError( 'getApplicationSupportPath() has not been implemented.'); } /// Path to the directory where application can store files that are persistent, /// backed up, and not visible to the user, such as sqlite.db. - Future getLibraryPath() { + Future getLibraryPath() { throw UnimplementedError('getLibraryPath() has not been implemented.'); } /// Path to a directory where the application may place data that is /// user-generated, or that cannot otherwise be recreated by your application. - Future getApplicationDocumentsPath() { + Future getApplicationDocumentsPath() { throw UnimplementedError( 'getApplicationDocumentsPath() has not been implemented.'); } @@ -67,7 +67,7 @@ abstract class PathProviderPlatform extends PlatformInterface { /// Path to a directory where the application may access top level storage. /// The current operating system should be determined before issuing this /// function call, as this functionality is only available on Android. - Future getExternalStoragePath() { + Future getExternalStoragePath() { throw UnimplementedError( 'getExternalStoragePath() has not been implemented.'); } @@ -76,7 +76,7 @@ abstract class PathProviderPlatform extends PlatformInterface { /// stored. These paths typically reside on external storage like separate /// partitions or SD cards. Phones may have multiple storage directories /// available. - Future> getExternalCachePaths() { + Future?> getExternalCachePaths() { throw UnimplementedError( 'getExternalCachePaths() has not been implemented.'); } @@ -84,10 +84,10 @@ abstract class PathProviderPlatform extends PlatformInterface { /// Paths to directories where application specific data can be stored. /// These paths typically reside on external storage like separate partitions /// or SD cards. Phones may have multiple storage directories available. - Future> getExternalStoragePaths({ + Future?> getExternalStoragePaths({ /// Optional parameter. See [StorageDirectory] for more informations on /// how this type translates to Android storage directories. - StorageDirectory type, + StorageDirectory? type, }) { throw UnimplementedError( 'getExternalStoragePaths() has not been implemented.'); @@ -95,7 +95,7 @@ abstract class PathProviderPlatform extends PlatformInterface { /// Path to the directory where downloaded files can be stored. /// This is typically only relevant on desktop operating systems. - Future getDownloadsPath() { + Future getDownloadsPath() { throw UnimplementedError('getDownloadsPath() has not been implemented.'); } } diff --git a/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart b/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart index 7826fa4365be..728c1068f876 100644 --- a/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart +++ b/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart @@ -30,34 +30,34 @@ class MethodChannelPathProvider extends PathProviderPlatform { _platform = platform; } - Future getTemporaryPath() { + Future getTemporaryPath() { return methodChannel.invokeMethod('getTemporaryDirectory'); } - Future getApplicationSupportPath() { + Future getApplicationSupportPath() { return methodChannel.invokeMethod('getApplicationSupportDirectory'); } - Future getLibraryPath() { + Future getLibraryPath() { if (!_platform.isIOS && !_platform.isMacOS) { throw UnsupportedError('Functionality only available on iOS/macOS'); } return methodChannel.invokeMethod('getLibraryDirectory'); } - Future getApplicationDocumentsPath() { + Future getApplicationDocumentsPath() { return methodChannel .invokeMethod('getApplicationDocumentsDirectory'); } - Future getExternalStoragePath() { + Future getExternalStoragePath() { if (!_platform.isAndroid) { throw UnsupportedError('Functionality only available on Android'); } return methodChannel.invokeMethod('getStorageDirectory'); } - Future> getExternalCachePaths() { + Future?> getExternalCachePaths() { if (!_platform.isAndroid) { throw UnsupportedError('Functionality only available on Android'); } @@ -65,8 +65,8 @@ class MethodChannelPathProvider extends PathProviderPlatform { .invokeListMethod('getExternalCacheDirectories'); } - Future> getExternalStoragePaths({ - StorageDirectory type, + Future?> getExternalStoragePaths({ + StorageDirectory? type, }) async { if (!_platform.isAndroid) { throw UnsupportedError('Functionality only available on Android'); @@ -77,7 +77,7 @@ class MethodChannelPathProvider extends PathProviderPlatform { ); } - Future getDownloadsPath() { + Future getDownloadsPath() { if (!_platform.isMacOS) { throw UnsupportedError('Functionality only available on macOS'); } diff --git a/packages/path_provider/path_provider_platform_interface/pubspec.yaml b/packages/path_provider/path_provider_platform_interface/pubspec.yaml index 36b539ba1077..946d2ed4b4fd 100644 --- a/packages/path_provider/path_provider_platform_interface/pubspec.yaml +++ b/packages/path_provider/path_provider_platform_interface/pubspec.yaml @@ -3,20 +3,20 @@ description: A common platform interface for the path_provider plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_platform_interface # 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: 1.0.4 +version: 2.0.0-nullsafety dependencies: flutter: sdk: flutter - meta: ^1.0.5 - platform: ">=2.0.0 <4.0.0" - plugin_platform_interface: ^1.0.1 + meta: ^1.3.0-nullsafety.3 + platform: ^3.0.0-nullsafety.4 + plugin_platform_interface: ^1.1.0-nullsafety dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.10.0" diff --git a/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart b/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart index 99c9349f9ae5..7130d7743e69 100644 --- a/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart +++ b/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart @@ -19,7 +19,7 @@ void main() { const String kDownloadsPath = 'downloadsPath'; group('$MethodChannelPathProvider', () { - MethodChannelPathProvider methodChannelPathProvider; + late MethodChannelPathProvider methodChannelPathProvider; final List log = []; setUp(() async { @@ -59,7 +59,7 @@ void main() { }); test('getTemporaryPath', () async { - final String path = await methodChannelPathProvider.getTemporaryPath(); + final String? path = await methodChannelPathProvider.getTemporaryPath(); expect( log, [isMethodCall('getTemporaryDirectory', arguments: null)], @@ -68,7 +68,7 @@ void main() { }); test('getApplicationSupportPath', () async { - final String path = + final String? path = await methodChannelPathProvider.getApplicationSupportPath(); expect( log, @@ -92,7 +92,7 @@ void main() { methodChannelPathProvider .setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - final String path = await methodChannelPathProvider.getLibraryPath(); + final String? path = await methodChannelPathProvider.getLibraryPath(); expect( log, [isMethodCall('getLibraryDirectory', arguments: null)], @@ -104,7 +104,7 @@ void main() { methodChannelPathProvider .setMockPathProviderPlatform(FakePlatform(operatingSystem: 'macos')); - final String path = await methodChannelPathProvider.getLibraryPath(); + final String? path = await methodChannelPathProvider.getLibraryPath(); expect( log, [isMethodCall('getLibraryDirectory', arguments: null)], @@ -113,7 +113,7 @@ void main() { }); test('getApplicationDocumentsPath', () async { - final String path = + final String? path = await methodChannelPathProvider.getApplicationDocumentsPath(); expect( log, @@ -125,13 +125,13 @@ void main() { }); test('getExternalCachePaths android succeeds', () async { - final List result = + final List? result = await methodChannelPathProvider.getExternalCachePaths(); expect( log, [isMethodCall('getExternalCacheDirectories', arguments: null)], ); - expect(result.length, 1); + expect(result!.length, 1); expect(result.first, kExternalCachePaths); }); @@ -147,10 +147,12 @@ void main() { } }); - for (StorageDirectory type - in StorageDirectory.values + [null]) { + for (StorageDirectory? type in [ + null, + ...StorageDirectory.values + ]) { test('getExternalStoragePaths (type: $type) android succeeds', () async { - final List result = + final List? result = await methodChannelPathProvider.getExternalStoragePaths(type: type); expect( log, @@ -162,7 +164,7 @@ void main() { ], ); - expect(result.length, 1); + expect(result!.length, 1); expect(result.first, kExternalStoragePaths); }); @@ -182,7 +184,7 @@ void main() { test('getDownloadsPath macos succeeds', () async { methodChannelPathProvider .setMockPathProviderPlatform(FakePlatform(operatingSystem: 'macos')); - final String result = await methodChannelPathProvider.getDownloadsPath(); + final String? result = await methodChannelPathProvider.getDownloadsPath(); expect( log, [isMethodCall('getDownloadsDirectory', arguments: null)], diff --git a/packages/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md index bdb0ae5b2d9f..ef1f5043a2b7 100644 --- a/packages/path_provider/path_provider_windows/CHANGELOG.md +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.4+4 + +* Update Flutter SDK constraint. + ## 0.0.4+3 * Remove unused `test` dependency. diff --git a/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart index 7ff448abf020..e1063957879e 100644 --- a/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart +++ b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart @@ -123,7 +123,11 @@ class PathProviderWindows extends PathProviderPlatform { GUID knownFolderID = GUID.fromString(folderID); final hr = SHGetKnownFolderPath( - knownFolderID.addressOf, KF_FLAG_DEFAULT, NULL, pathPtrPtr); + knownFolderID.addressOf, // ignore: deprecated_member_use + KF_FLAG_DEFAULT, + NULL, + pathPtrPtr, + ); if (FAILED(hr)) { if (hr == E_INVALIDARG || hr == E_FAIL) { diff --git a/packages/path_provider/path_provider_windows/pubspec.yaml b/packages/path_provider/path_provider_windows/pubspec.yaml index 342774680dc4..62185f42e765 100644 --- a/packages/path_provider/path_provider_windows/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/pubspec.yaml @@ -1,7 +1,7 @@ name: path_provider_windows description: Windows implementation of the path_provider plugin homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_windows -version: 0.0.4+3 +version: 0.0.4+4 flutter: plugin: @@ -26,4 +26,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + flutter: ">=1.12.13+hotfix.4" diff --git a/packages/plugin_platform_interface/CHANGELOG.md b/packages/plugin_platform_interface/CHANGELOG.md index 01b5ff7d1252..7df1834966dd 100644 --- a/packages/plugin_platform_interface/CHANGELOG.md +++ b/packages/plugin_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.1.0-nullsafety.1 + +* Bump Dart SDK to support null safety. + +## 1.1.0-nullsafety + +* Migrate to null safety. + ## 1.0.3 * Fix homepage in `pubspec.yaml`. diff --git a/packages/plugin_platform_interface/lib/plugin_platform_interface.dart b/packages/plugin_platform_interface/lib/plugin_platform_interface.dart index be4871928686..cd87b04dc739 100644 --- a/packages/plugin_platform_interface/lib/plugin_platform_interface.dart +++ b/packages/plugin_platform_interface/lib/plugin_platform_interface.dart @@ -41,7 +41,7 @@ import 'package:meta/meta.dart'; /// [MockPlatformInterfaceMixin] for a sample of using Mockito to mock a platform interface. abstract class PlatformInterface { /// Pass a private, class-specific `const Object()` as the `token`. - PlatformInterface({@required Object token}) : _instanceToken = token; + PlatformInterface({required Object token}) : _instanceToken = token; final Object _instanceToken; diff --git a/packages/plugin_platform_interface/pubspec.yaml b/packages/plugin_platform_interface/pubspec.yaml index ae11b144ca8c..05fc918bf9b5 100644 --- a/packages/plugin_platform_interface/pubspec.yaml +++ b/packages/plugin_platform_interface/pubspec.yaml @@ -12,17 +12,17 @@ description: Reusable base class for Flutter plugin platform interfaces. # be done when absolutely necessary and after the ecosystem has already migrated to 1.X.Y version # that is forward compatible with 2.0.0 (ideally the ecosystem have migrated to depend on: # `plugin_platform_interface: >=1.X.Y <3.0.0`). -version: 1.0.3 +version: 1.1.0-nullsafety.1 repository: https://github.com/flutter/plugins/tree/master/packages/plugin_platform_interface environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0-0 <3.0.0" dependencies: - meta: ^1.0.0 + meta: ^1.3.0-nullsafety.3 dev_dependencies: mockito: ^4.1.1 - test: ^1.9.4 - pedantic: ^1.8.0 + test: ^1.10.0-nullsafety.1 + pedantic: ^1.10.0-nullsafety.1 diff --git a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart index 0488c20f3efb..b07dd4dcede1 100644 --- a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart +++ b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(egarciad): Remove once Mockito is migrated to null safety. +// @dart = 2.9 import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; diff --git a/packages/quick_actions/CHANGELOG.md b/packages/quick_actions/CHANGELOG.md index 0742d73f500a..a0c1b1f43c66 100644 --- a/packages/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.4.0+12 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.4.0+11 + +* Update Flutter SDK constraint. + ## 0.4.0+10 * Update android compileSdkVersion to 29. diff --git a/packages/quick_actions/README.md b/packages/quick_actions/README.md index 21e7cfb619cb..4573523a5e36 100644 --- a/packages/quick_actions/README.md +++ b/packages/quick_actions/README.md @@ -43,6 +43,6 @@ quick action. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/quick_actions/pubspec.yaml b/packages/quick_actions/pubspec.yaml index 187c5707ddea..e76cd9ce8bd2 100644 --- a/packages/quick_actions/pubspec.yaml +++ b/packages/quick_actions/pubspec.yaml @@ -2,7 +2,7 @@ name: quick_actions description: Flutter plugin for creating shortcuts on home screen, also known as Quick Actions on iOS and App Shortcuts on Android. homepage: https://github.com/flutter/plugins/tree/master/packages/quick_actions -version: 0.4.0+10 +version: 0.4.0+12 flutter: plugin: @@ -29,4 +29,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/sensors/CHANGELOG.md b/packages/sensors/CHANGELOG.md index a2693dd7ed0b..8970f1e76e5d 100644 --- a/packages/sensors/CHANGELOG.md +++ b/packages/sensors/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.4.2+8 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.4.2+7 + +* Update Flutter SDK constraint. + ## 0.4.2+6 * Update android compileSdkVersion to 29. diff --git a/packages/sensors/README.md b/packages/sensors/README.md index e3c80b2b2947..08a9b2ea2b8c 100644 --- a/packages/sensors/README.md +++ b/packages/sensors/README.md @@ -13,7 +13,7 @@ A Flutter plugin to access the accelerometer and gyroscope sensors. ## Usage To use this plugin, add `sensors` as a [dependency in your pubspec.yaml -file](https://flutter.io/platform-plugins/). +file](https://flutter.dev/docs/development/platform-integration/platform-channels). This will expose three classes of sensor events, through three different streams. diff --git a/packages/sensors/pubspec.yaml b/packages/sensors/pubspec.yaml index 4e830d9d0480..35feb4b1ed56 100644 --- a/packages/sensors/pubspec.yaml +++ b/packages/sensors/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/sensors # 0.4.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.4.2+6 +version: 0.4.2+8 flutter: plugin: @@ -31,4 +31,4 @@ dev_dependencies: environment: sdk: ">=2.1.0<3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md index 883cd340ecfb..855e737a1cd4 100644 --- a/packages/share/CHANGELOG.md +++ b/packages/share/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.0.0-nullsafety.2 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 2.0.0-nullsafety.1 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 2.0.0-nullsafety + +* Migrate to null safety. + +## 0.6.5+5 + +* Update Flutter SDK constraint. + ## 0.6.5+4 * Fix iPad share window not showing when `origin` is null. diff --git a/packages/share/README.md b/packages/share/README.md index 750fca6a5b18..6ca38b416f03 100644 --- a/packages/share/README.md +++ b/packages/share/README.md @@ -1,6 +1,6 @@ # Share plugin -[![pub package](https://img.shields.io/pub/v/share.svg)](https://pub.dartlang.org/packages/share) +[![pub package](https://img.shields.io/pub/v/share.svg)](https://pub.dev/packages/share) A Flutter plugin to share content from your Flutter app via the platform's share dialog. @@ -17,7 +17,7 @@ For more details see: https://github.com/flutter/flutter/wiki/Package-migration- ## Usage -To use this plugin, add `share` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add `share` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). ## Example diff --git a/packages/share/example/ios/Runner.xcodeproj/project.pbxproj b/packages/share/example/ios/Runner.xcodeproj/project.pbxproj index 639666b2865c..d03ef3e65776 100644 --- a/packages/share/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/share/example/ios/Runner.xcodeproj/project.pbxproj @@ -197,7 +197,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 12A149CFB1B2610A83692801 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -268,24 +267,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 12A149CFB1B2610A83692801 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../Flutter/Flutter.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index 7ebce3ce5591..a9ebd6bb79fb 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -78,7 +78,7 @@ class DemoAppState extends State { const Padding(padding: EdgeInsets.only(top: 12.0)), Builder( builder: (BuildContext context) { - return RaisedButton( + return ElevatedButton( child: const Text('Share'), onPressed: text.isEmpty && imagePaths.isEmpty ? null @@ -89,7 +89,7 @@ class DemoAppState extends State { const Padding(padding: EdgeInsets.only(top: 12.0)), Builder( builder: (BuildContext context) { - return RaisedButton( + return ElevatedButton( child: const Text('Share With Empty Origin'), onPressed: () => _onShareWithEmptyOrigin(context), ); @@ -110,11 +110,11 @@ class DemoAppState extends State { _onShare(BuildContext context) async { // A builder is used to retrieve the context immediately - // surrounding the RaisedButton. + // surrounding the ElevatedButton. // // The context's `findRenderObject` returns the first // RenderObject in its descendent tree when it's not - // a RenderObjectWidget. The RaisedButton's RenderObject + // a RenderObjectWidget. The ElevatedButton's RenderObject // has its position and size after it's built. final RenderBox box = context.findRenderObject(); diff --git a/packages/share/integration_test/share_test.dart b/packages/share/integration_test/share_test.dart index 08a19ce2c27b..7b66480d0681 100644 --- a/packages/share/integration_test/share_test.dart +++ b/packages/share/integration_test/share_test.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// @dart = 2.9 + import 'package:flutter_test/flutter_test.dart'; import 'package:share/share.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index 4a3ff6f1de09..f15566714857 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -33,8 +33,8 @@ class Share { /// from [MethodChannel]. static Future share( String text, { - String subject, - Rect sharePositionOrigin, + String? subject, + Rect? sharePositionOrigin, }) { assert(text != null); assert(text.isNotEmpty); @@ -67,10 +67,10 @@ class Share { /// from [MethodChannel]. static Future shareFiles( List paths, { - List mimeTypes, - String subject, - String text, - Rect sharePositionOrigin, + List? mimeTypes, + String? subject, + String? text, + Rect? sharePositionOrigin, }) { assert(paths != null); assert(paths.isNotEmpty); diff --git a/packages/share/pubspec.yaml b/packages/share/pubspec.yaml index 5bd79effe4a7..ca9b506bf35c 100644 --- a/packages/share/pubspec.yaml +++ b/packages/share/pubspec.yaml @@ -2,10 +2,7 @@ name: share description: Flutter plugin for sharing content via the platform share UI, using the ACTION_SEND intent on Android and UIActivityViewController on iOS. homepage: https://github.com/flutter/plugins/tree/master/packages/share -# 0.6.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.6.5+4 +version: 2.0.0-nullsafety.2 flutter: plugin: @@ -17,20 +14,20 @@ flutter: pluginClass: FLTSharePlugin dependencies: - meta: ^1.0.5 - mime: ^0.9.7 + meta: ^1.3.0-nullsafety.6 + mime: ^1.0.0-nullsafety.0 flutter: sdk: flutter dev_dependencies: - test: ^1.3.0 - mockito: ^3.0.0 + test: ^1.16.0-nullsafety.13 + mockito: ^4.1.3 flutter_test: sdk: flutter integration_test: path: ../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.3 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + flutter: ">=1.12.13+hotfix.5" + sdk: ">=2.12.0-0 <3.0.0" diff --git a/packages/share/test/share_test.dart b/packages/share/test/share_test.dart index e862d1baf579..d00867b19452 100644 --- a/packages/share/test/share_test.dart +++ b/packages/share/test/share_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// @dart = 2.9 + import 'dart:io'; import 'dart:ui'; @@ -26,14 +28,6 @@ void main() { }); }); - test('sharing null fails', () { - expect( - () => Share.share(null), - throwsA(const TypeMatcher()), - ); - verifyZeroInteractions(mockChannel); - }); - test('sharing empty fails', () { expect( () => Share.share(''), @@ -58,14 +52,6 @@ void main() { })); }); - test('sharing null file fails', () { - expect( - () => Share.shareFiles([null]), - throwsA(const TypeMatcher()), - ); - verifyZeroInteractions(mockChannel); - }); - test('sharing empty file fails', () { expect( () => Share.shareFiles(['']), diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 64a74e4ac684..5e09b3f87fb0 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.5.13+2 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.5.13+1 + +* Update Flutter SDK constraint. + +## 0.5.13 + +* Update integration test examples to use `testWidgets` instead of `test`. + ## 0.5.12+4 * Remove unused `test` dependency. diff --git a/packages/shared_preferences/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md index 9ccac0ac49ae..516d7a91b848 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -1,6 +1,6 @@ # Shared preferences plugin -[![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dartlang.org/packages/shared_preferences) +[![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dev/packages/shared_preferences) Wraps platform-specific persistent storage for simple data (NSUserDefaults on iOS and macOS, SharedPreferences on Android, etc.). Data may be persisted to disk asynchronously, @@ -16,7 +16,7 @@ Please use `shared_preferences: '>=0.5.y+x <2.0.0'` as your dependency constrain For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 ## Usage -To use this plugin, add `shared_preferences` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add `shared_preferences` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). ### Example diff --git a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart index 0d49ed95dd2d..e43d4e3ae0c2 100644 --- a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart @@ -33,7 +33,7 @@ void main() { preferences.clear(); }); - test('reading', () async { + testWidgets('reading', (WidgetTester _) async { expect(preferences.get('String'), isNull); expect(preferences.get('bool'), isNull); expect(preferences.get('int'), isNull); @@ -46,7 +46,7 @@ void main() { expect(preferences.getStringList('List'), isNull); }); - test('writing', () async { + testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setString('String', kTestValues2['flutter.String']), preferences.setBool('bool', kTestValues2['flutter.bool']), @@ -61,7 +61,7 @@ void main() { expect(preferences.getStringList('List'), kTestValues2['flutter.List']); }); - test('removing', () async { + testWidgets('removing', (WidgetTester _) async { const String key = 'testKey'; await preferences.setString(key, kTestValues['flutter.String']); await preferences.setBool(key, kTestValues['flutter.bool']); @@ -72,7 +72,7 @@ void main() { expect(preferences.get('testKey'), isNull); }); - test('clearing', () async { + testWidgets('clearing', (WidgetTester _) async { await preferences.setString('String', kTestValues['flutter.String']); await preferences.setBool('bool', kTestValues['flutter.bool']); await preferences.setInt('int', kTestValues['flutter.int']); diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index c09e2c64d7f9..69689c738b8e 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/shared_prefere # 0.5.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.5.12+4 +version: 0.5.13+2 flutter: plugin: @@ -48,4 +48,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + flutter: ">=1.12.13+hotfix.5" 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 763d32642cfa..80faba404154 100755 --- a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 +// @dart = 2.9 import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index dd67393b07b0..9821b79c5cc2 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.0.3+1 + +* Update Flutter SDK constraint. + +## 0.0.3 + +* Update integration test examples to use `testWidgets` instead of `test`. + ## 0.0.2+4 * Remove unused `test` dependency. diff --git a/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart index 0d49ed95dd2d..e43d4e3ae0c2 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart @@ -33,7 +33,7 @@ void main() { preferences.clear(); }); - test('reading', () async { + testWidgets('reading', (WidgetTester _) async { expect(preferences.get('String'), isNull); expect(preferences.get('bool'), isNull); expect(preferences.get('int'), isNull); @@ -46,7 +46,7 @@ void main() { expect(preferences.getStringList('List'), isNull); }); - test('writing', () async { + testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setString('String', kTestValues2['flutter.String']), preferences.setBool('bool', kTestValues2['flutter.bool']), @@ -61,7 +61,7 @@ void main() { expect(preferences.getStringList('List'), kTestValues2['flutter.List']); }); - test('removing', () async { + testWidgets('removing', (WidgetTester _) async { const String key = 'testKey'; await preferences.setString(key, kTestValues['flutter.String']); await preferences.setBool(key, kTestValues['flutter.bool']); @@ -72,7 +72,7 @@ void main() { expect(preferences.get('testKey'), isNull); }); - test('clearing', () async { + testWidgets('clearing', (WidgetTester _) async { await preferences.setString('String', kTestValues['flutter.String']); await preferences.setBool('bool', kTestValues['flutter.bool']); await preferences.setInt('int', kTestValues['flutter.int']); diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index 380cee9efb1c..50709aac5f8a 100644 --- a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml @@ -1,6 +1,6 @@ name: shared_preferences_linux description: Linux implementation of the shared_preferences plugin -version: 0.0.2+4 +version: 0.0.3+1 homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_linux flutter: @@ -12,7 +12,7 @@ flutter: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + flutter: ">=1.12.8" dependencies: file: ">=5.1.0 <7.0.0" diff --git a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md index 177f1f2e02e2..3eff6db6949e 100644 --- a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.1+12 + +* Update Flutter SDK constraint. + ## 0.0.1+11 * Remove unused `test` dependency. diff --git a/packages/shared_preferences/shared_preferences_macos/pubspec.yaml b/packages/shared_preferences/shared_preferences_macos/pubspec.yaml index b161327e3f3d..afc5b9ec0b4e 100644 --- a/packages/shared_preferences/shared_preferences_macos/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_macos/pubspec.yaml @@ -3,7 +3,7 @@ description: macOS implementation of the shared_preferences plugin. # 0.0.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.0.1+11 +version: 0.0.1+12 homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_macos flutter: @@ -14,7 +14,7 @@ flutter: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + flutter: ">=1.12.8" dependencies: shared_preferences_platform_interface: ^1.0.0 diff --git a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md index 5fe7b18160ba..88d3a9ac5f00 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.5 + +* Update Flutter SDK constraint. + ## 1.0.4 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml index 3b8f2bfcada4..da31497df1c6 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml @@ -1,7 +1,7 @@ name: shared_preferences_platform_interface description: A common platform interface for the shared_preferences plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_platform_interface -version: 1.0.4 +version: 1.0.5 dependencies: meta: ^1.0.4 @@ -15,4 +15,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + flutter: ">=1.12.8" diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index b2076dd8c17e..2ba877856da6 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.1.2+8 + +* Update Flutter SDK constraint. + ## 0.1.2+7 * Removed Android folder from `shared_preferences_web`. diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml index a153135b6da7..d657b2300727 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/shared_prefere # 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.1.2+7 +version: 0.1.2+8 flutter: plugin: @@ -28,4 +28,4 @@ dev_dependencies: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + flutter: ">=1.12.13+hotfix.4" diff --git a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md index 8604db297ca6..ecc790ef9bab 100644 --- a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.0.2+1 + +* Update Flutter SDK constraint. + +## 0.0.2 + +* Update integration test examples to use `testWidgets` instead of `test`. + ## 0.0.1+3 * Remove unused `test` dependency. diff --git a/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart index 44c8129e9405..016a21f70fe3 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart @@ -37,7 +37,7 @@ void main() { preferences.clear(); }); - test('reading', () async { + testWidgets('reading', (WidgetTester _) async { final Map values = await preferences.getAll(); expect(values['String'], isNull); expect(values['bool'], isNull); @@ -46,7 +46,7 @@ void main() { expect(values['List'], isNull); }); - test('writing', () async { + testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( 'String', 'String', kTestValues2['flutter.String']), @@ -64,7 +64,7 @@ void main() { expect(values['List'], kTestValues2['flutter.List']); }); - test('removing', () async { + testWidgets('removing', (WidgetTester _) async { const String key = 'testKey'; await preferences.setValue('String', key, kTestValues['flutter.String']); await preferences.setValue('Bool', key, kTestValues['flutter.bool']); @@ -77,7 +77,7 @@ void main() { expect(values[key], isNull); }); - test('clearing', () async { + testWidgets('clearing', (WidgetTester _) async { await preferences.setValue( 'String', 'String', kTestValues['flutter.String']); await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']); diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml index 0de70737bc19..ce559d5ea3fb 100644 --- a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml @@ -1,7 +1,7 @@ name: shared_preferences_windows description: Windows implementation of shared_preferences homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_windows -version: 0.0.1+3 +version: 0.0.2+1 flutter: plugin: @@ -12,7 +12,7 @@ flutter: environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + flutter: ">=1.12.8" dependencies: shared_preferences_platform_interface: ^1.0.0 diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 832c0c62f553..73852cdfea12 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,35 @@ +## 6.0.0-nullsafety.4 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 6.0.0-nullsafety.3 + +* forceSafariVC should be nullable. + +## 6.0.0-nullsafety.2 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 6.0.0-nullsafety.1 + +* Bump Dart SDK to support null safety. + +## 6.0.0-nullsafety + +* Migrate to null safety. + +## 5.7.13 + +* Update Flutter SDK constraint. + +## 5.7.12 + +* Updated code sample in `README.md` + +## 5.7.11 + +* Update integration test examples to use `testWidgets` instead of `test`. + ## 5.7.10 * Update Dart SDK constraint in example. diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index 811dcd5b4ea1..573624fa18eb 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -1,6 +1,6 @@ # url_launcher -[![pub package](https://img.shields.io/pub/v/url_launcher.svg)](https://pub.dartlang.org/packages/url_launcher) +[![pub package](https://img.shields.io/pub/v/url_launcher.svg)](https://pub.dev/packages/url_launcher) A Flutter plugin for launching a URL in the mobile platform. Supports iOS, Android, web, Windows, macOS, and Linux. @@ -14,31 +14,28 @@ To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml fil import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; -void main() { - runApp(Scaffold( - body: Center( - child: RaisedButton( - onPressed: _launchURL, - child: Text('Show Flutter homepage'), +const _url = 'https://flutter.dev'; + +void main() => runApp( + const MaterialApp( + home: Material( + child: Center( + child: RaisedButton( + onPressed: _launchURL, + child: Text('Show Flutter homepage'), + ), + ), + ), ), - ), - )); -} - -_launchURL() async { - const url = 'https://flutter.dev'; - if (await canLaunch(url)) { - await launch(url); - } else { - throw 'Could not launch $url'; - } -} + ); +void _launchURL() async => + await canLaunch(_url) ? await launch(_url) : throw 'Could not launch $_url'; ``` ## Supported URL schemes -The [`launch`](https://www.dartdocs.org/documentation/url_launcher/latest/url_launcher/launch.html) method +The [`launch`](https://pub.dev/documentation/url_launcher/latest/url_launcher/launch.html) method takes a string argument containing a URL. This URL can be formatted using a number of different URL schemes. The supported URL schemes depend on the underlying platform and installed apps. @@ -83,7 +80,7 @@ launching a URL using the `sms` scheme, or a device may not have an email app and thus no support for launching a URL using the `email` scheme. We recommend checking which URL schemes are supported using the -[`canLaunch`](https://www.dartdocs.org/documentation/url_launcher/latest/url_launcher/canLaunch.html) +[`canLaunch`](https://pub.dev/documentation/url_launcher/latest/url_launcher/canLaunch.html) method prior to calling `launch`. If the `canLaunch` method returns false, as a best practice we suggest adjusting the application UI so that the unsupported URL is never triggered; for example, if the `email` scheme is not supported, a diff --git a/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart index 9fb5eaa34b4c..80d21b740c1e 100644 --- a/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart @@ -2,6 +2,9 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// TODO(egarciad): Remove once integration_test is migrated to null safety. +// @dart = 2.9 + import 'dart:io' show Platform; import 'package:flutter/foundation.dart' show kIsWeb; @@ -12,7 +15,7 @@ import 'package:url_launcher/url_launcher.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - test('canLaunch', () async { + testWidgets('canLaunch', (WidgetTester _) async { expect(await canLaunch('randomstring'), false); // Generally all devices should have some default browser. diff --git a/packages/url_launcher/url_launcher/example/lib/main.dart b/packages/url_launcher/url_launcher/example/lib/main.dart index f7d90c4bef65..0b310490d0e9 100644 --- a/packages/url_launcher/url_launcher/example/lib/main.dart +++ b/packages/url_launcher/url_launcher/example/lib/main.dart @@ -27,7 +27,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override @@ -35,7 +35,7 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - Future _launched; + Future? _launched; String _phone = ''; Future _launchInBrowser(String url) async { @@ -141,7 +141,7 @@ class _MyHomePageState extends State { decoration: const InputDecoration( hintText: 'Input the phone number to launch')), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _makePhoneCall('tel:$_phone'); }), @@ -151,33 +151,33 @@ class _MyHomePageState extends State { padding: EdgeInsets.all(16.0), child: Text(toLaunch), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInBrowser(toLaunch); }), child: const Text('Launch in browser'), ), const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewOrVC(toLaunch); }), child: const Text('Launch in app'), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewWithJavaScript(toLaunch); }), child: const Text('Launch in app(JavaScript ON)'), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewWithDomStorage(toLaunch); }), child: const Text('Launch in app(DOM storage ON)'), ), const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchUniversalLinkIos(toLaunch); }), @@ -185,7 +185,7 @@ class _MyHomePageState extends State { 'Launch a universal link in a native app, fallback to Safari.(Youtube)'), ), const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewOrVC(toLaunch); Timer(const Duration(seconds: 5), () { diff --git a/packages/url_launcher/url_launcher/example/pubspec.yaml b/packages/url_launcher/url_launcher/example/pubspec.yaml index 94df1a4b4959..7caea27744db 100644 --- a/packages/url_launcher/url_launcher/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher/example/pubspec.yaml @@ -12,13 +12,13 @@ dev_dependencies: path: ../../../integration_test flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 mockito: ^4.1.1 - plugin_platform_interface: ^1.0.0 + plugin_platform_interface: ^1.1.0-nullsafety.1 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0-0 <3.0.0" flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/packages/url_launcher/url_launcher/example/test/url_launcher_example_test.dart b/packages/url_launcher/url_launcher/example/test/url_launcher_example_test.dart index 41b9f6f5ec6c..a890be7f65f1 100644 --- a/packages/url_launcher/url_launcher/example/test/url_launcher_example_test.dart +++ b/packages/url_launcher/url_launcher/example/test/url_launcher_example_test.dart @@ -1,3 +1,10 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(egarciad): Remove once mockito is migrated to null safety. +// @dart = 2.9 + import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import 'package:mockito/mockito.dart'; @@ -25,7 +32,7 @@ void main() { headers: defaultHeaders)); Finder browserlaunchBtn = - find.widgetWithText(RaisedButton, 'Launch in browser'); + find.widgetWithText(ElevatedButton, 'Launch in browser'); expect(browserlaunchBtn, findsOneWidget); await tester.tap(browserlaunchBtn); diff --git a/packages/url_launcher/url_launcher/example/test_driver/integration_test.dart b/packages/url_launcher/url_launcher/example/test_driver/integration_test.dart index 7a2c21338786..e56756f38cbd 100644 --- a/packages/url_launcher/url_launcher/example/test_driver/integration_test.dart +++ b/packages/url_launcher/url_launcher/example/test_driver/integration_test.dart @@ -2,6 +2,9 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// TODO(egarciad): Remove once flutter_driver is migrated to null safety. +// @dart = 2.9 + import 'dart:async'; import 'dart:convert'; import 'dart:io'; diff --git a/packages/url_launcher/url_launcher/lib/src/link.dart b/packages/url_launcher/url_launcher/lib/src/link.dart index bd54789accfb..14fdc9055d7a 100644 --- a/packages/url_launcher/url_launcher/lib/src/link.dart +++ b/packages/url_launcher/url_launcher/lib/src/link.dart @@ -17,7 +17,7 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. /// ```dart /// Link( /// uri: Uri.parse('https://flutter.dev'), -/// builder: (BuildContext context, FollowLink followLink) => RaisedButton( +/// builder: (BuildContext context, FollowLink followLink) => ElevatedButton( /// onPressed: followLink, /// // ... other properties here ... /// )}, @@ -29,7 +29,7 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. /// ```dart /// Link( /// uri: Uri.parse('/home'), -/// builder: (BuildContext context, FollowLink followLink) => RaisedButton( +/// builder: (BuildContext context, FollowLink followLink) => ElevatedButton( /// onPressed: followLink, /// // ... other properties here ... /// )}, @@ -40,7 +40,7 @@ class Link extends StatelessWidget implements LinkInfo { final LinkWidgetBuilder builder; /// The destination that this link leads to. - final Uri uri; + final Uri? uri; /// The target indicating where to open the link. final LinkTarget target; @@ -51,12 +51,11 @@ class Link extends StatelessWidget implements LinkInfo { /// Creates a widget that renders a real link on the web, and uses WebViews in /// native platforms to open links. Link({ - Key key, - @required this.uri, - LinkTarget target, - @required this.builder, - }) : target = target ?? LinkTarget.defaultTarget, - super(key: key); + Key? key, + required this.uri, + this.target = LinkTarget.defaultTarget, + required this.builder, + }) : super(key: key); LinkDelegate get _effectiveDelegate { return UrlLauncherPlatform.instance.linkDelegate ?? @@ -90,16 +89,17 @@ class DefaultLinkDelegate extends StatelessWidget { bool get _useWebView { if (link.target == LinkTarget.self) return true; if (link.target == LinkTarget.blank) return false; - return null; + return false; } Future _followLink(BuildContext context) async { - if (!link.uri.hasScheme) { + if (!link.uri!.hasScheme) { // A uri that doesn't have a scheme is an internal route name. In this // case, we push it via Flutter's navigation system instead of letting the // browser handle it. final String routeName = link.uri.toString(); - return pushRouteNameToFramework(context, routeName); + await pushRouteNameToFramework(context, routeName); + return; } // At this point, we know that the link is external. So we use the `launch` @@ -119,7 +119,6 @@ class DefaultLinkDelegate extends StatelessWidget { context: ErrorDescription('during launching a link'), )); } - return Future.value(null); } @override diff --git a/packages/url_launcher/url_launcher/lib/url_launcher.dart b/packages/url_launcher/url_launcher/lib/url_launcher.dart index 25aa623a590f..35833de18738 100644 --- a/packages/url_launcher/url_launcher/lib/url_launcher.dart +++ b/packages/url_launcher/url_launcher/lib/url_launcher.dart @@ -62,16 +62,15 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. /// is set to true and the universal link failed to launch. Future launch( String urlString, { - bool forceSafariVC, - bool forceWebView, - bool enableJavaScript, - bool enableDomStorage, - bool universalLinksOnly, - Map headers, - Brightness statusBarBrightness, - String webOnlyWindowName, + bool? forceSafariVC, + bool forceWebView = false, + bool enableJavaScript = false, + bool enableDomStorage = false, + bool universalLinksOnly = false, + Map headers = const {}, + Brightness? statusBarBrightness, + String? webOnlyWindowName, }) async { - assert(urlString != null); final Uri url = Uri.parse(urlString.trimLeft()); final bool isWebURL = url.scheme == 'http' || url.scheme == 'https'; if ((forceSafariVC == true || forceWebView == true) && !isWebURL) { @@ -84,29 +83,32 @@ Future launch( /// [true] so that ui is automatically computed if [statusBarBrightness] is set. bool previousAutomaticSystemUiAdjustment = true; if (statusBarBrightness != null && - defaultTargetPlatform == TargetPlatform.iOS) { + defaultTargetPlatform == TargetPlatform.iOS && + WidgetsBinding.instance != null) { previousAutomaticSystemUiAdjustment = - WidgetsBinding.instance.renderView.automaticSystemUiAdjustment; - WidgetsBinding.instance.renderView.automaticSystemUiAdjustment = false; + WidgetsBinding.instance!.renderView.automaticSystemUiAdjustment; + WidgetsBinding.instance!.renderView.automaticSystemUiAdjustment = false; SystemChrome.setSystemUIOverlayStyle(statusBarBrightness == Brightness.light ? SystemUiOverlayStyle.dark : SystemUiOverlayStyle.light); } + final bool result = await UrlLauncherPlatform.instance.launch( urlString, useSafariVC: forceSafariVC ?? isWebURL, - useWebView: forceWebView ?? false, - enableJavaScript: enableJavaScript ?? false, - enableDomStorage: enableDomStorage ?? false, - universalLinksOnly: universalLinksOnly ?? false, - headers: headers ?? {}, + useWebView: forceWebView, + enableJavaScript: enableJavaScript, + enableDomStorage: enableDomStorage, + universalLinksOnly: universalLinksOnly, + headers: headers, webOnlyWindowName: webOnlyWindowName, ); - assert(previousAutomaticSystemUiAdjustment != null); - if (statusBarBrightness != null) { - WidgetsBinding.instance.renderView.automaticSystemUiAdjustment = + + if (statusBarBrightness != null && WidgetsBinding.instance != null) { + WidgetsBinding.instance!.renderView.automaticSystemUiAdjustment = previousAutomaticSystemUiAdjustment; } + return result; } @@ -118,9 +120,6 @@ Future launch( /// For more information see the [Managing package visibility](https://developer.android.com/training/basics/intents/package-visibility) /// article in the Android docs. Future canLaunch(String urlString) async { - if (urlString == null) { - return false; - } return await UrlLauncherPlatform.instance.canLaunch(urlString); } diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 7fa97388ef91..871c43ced733 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher description: Flutter plugin for launching a URL on Android and iOS. Supports web, phone, SMS, and email schemes. homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher -version: 5.7.10 +version: 6.0.0-nullsafety.4 flutter: plugin: @@ -12,8 +12,9 @@ flutter: pluginClass: UrlLauncherPlugin ios: pluginClass: FLTURLLauncherPlugin - web: - default_package: url_launcher_web + # TODO(mvanbeusekom): Temporary disabled until web is migrated to nnbd (advised by @blasten). + #web: + # default_package: url_launcher_web linux: default_package: url_laucher_linux macos: @@ -24,25 +25,26 @@ flutter: dependencies: flutter: sdk: flutter - url_launcher_platform_interface: ^1.0.9 + url_launcher_platform_interface: ^2.0.0-nullsafety # The design on https://flutter.dev/go/federated-plugins was to leave # this constraint as "any". We cannot do it right now as it fails pub publish # validation, so we set a ^ constraint. # TODO(amirh): Revisit this (either update this part in the design or the pub tool). # https://github.com/flutter/flutter/issues/46264 - url_launcher_web: ^0.1.5 - url_launcher_linux: ^0.0.1 - url_launcher_macos: ^0.0.1 - url_launcher_windows: ^0.0.1 + url_launcher_linux: ^0.1.0-nullsafety + url_launcher_macos: ^0.1.0-nullsafety + url_launcher_windows: ^0.1.0-nullsafety + # TODO(mvanbeusekom): Temporary disabled until web is migrated to nnbd (advised by @blasten). + #url_launcher_web: ^0.1.3 dev_dependencies: flutter_test: sdk: flutter - test: ^1.3.0 + test: ^1.10.0-nullsafety.1 mockito: ^4.1.1 - plugin_platform_interface: ^1.0.0 - pedantic: ^1.8.0 + plugin_platform_interface: ^1.1.0-nullsafety.1 + pedantic: ^1.10.0-nullsafety.1 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/url_launcher/url_launcher/test/link_test.dart b/packages/url_launcher/url_launcher/test/link_test.dart index d525153dc0a0..46903aadaede 100644 --- a/packages/url_launcher/url_launcher/test/link_test.dart +++ b/packages/url_launcher/url_launcher/test/link_test.dart @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 +// TODO(egarciad): Remove once Mockito has been migrated to null safety. +// @dart = 2.9 import 'dart:ui'; import 'package:flutter/material.dart'; diff --git a/packages/url_launcher/url_launcher/test/url_launcher_test.dart b/packages/url_launcher/url_launcher/test/url_launcher_test.dart index f18e16c7e318..89a7801e1ca8 100644 --- a/packages/url_launcher/url_launcher/test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher/test/url_launcher_test.dart @@ -2,10 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 +// TODO(mvanbeusekom): Remove once Mockito is migrated to null safety. +// @dart = 2.9 import 'dart:async'; import 'dart:ui'; + import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:flutter/foundation.dart'; @@ -41,10 +43,6 @@ void main() { }); }); group('launch', () { - test('requires a non-null urlString', () { - expect(() => launch(null), throwsAssertionError); - }); - test('default behavior', () async { await launch('http://flutter.dev/'); expect( diff --git a/packages/url_launcher/url_launcher_linux/CHANGELOG.md b/packages/url_launcher/url_launcher_linux/CHANGELOG.md index e0a01ec629f5..2d5a9a7d05af 100644 --- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -1,3 +1,23 @@ +## 0.1.0-nullsafety.3 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.1.0-nullsafety.2 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.1.0-nullsafety.1 + +* Migrate to null safety. + +## 0.0.2+1 + +* Update Flutter SDK constraint. + +## 0.0.2 + +* Update integration test examples to use `testWidgets` instead of `test`. + ## 0.0.1+4 * Update Dart SDK constraint in example. diff --git a/packages/url_launcher/url_launcher_linux/example/README.md b/packages/url_launcher/url_launcher_linux/example/README.md index 28dd90d71700..c200da8974d1 100644 --- a/packages/url_launcher/url_launcher_linux/example/README.md +++ b/packages/url_launcher/url_launcher_linux/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the url_launcher plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/url_launcher/url_launcher_linux/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher_linux/example/integration_test/url_launcher_test.dart index 0b25516c2a29..d11ddb49966b 100644 --- a/packages/url_launcher/url_launcher_linux/example/integration_test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher_linux/example/integration_test/url_launcher_test.dart @@ -9,7 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - test('canLaunch', () async { + testWidgets('canLaunch', (WidgetTester _) async { expect(await canLaunch('randomstring'), false); // Generally all devices should have some default browser. diff --git a/packages/url_launcher/url_launcher_linux/example/lib/main.dart b/packages/url_launcher/url_launcher_linux/example/lib/main.dart index b5cce7482d07..a45862012328 100644 --- a/packages/url_launcher/url_launcher_linux/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_linux/example/lib/main.dart @@ -140,7 +140,7 @@ class _MyHomePageState extends State { decoration: const InputDecoration( hintText: 'Input the phone number to launch')), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _makePhoneCall('tel:$_phone'); }), @@ -150,33 +150,33 @@ class _MyHomePageState extends State { padding: EdgeInsets.all(16.0), child: Text(toLaunch), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInBrowser(toLaunch); }), child: const Text('Launch in browser'), ), const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewOrVC(toLaunch); }), child: const Text('Launch in app'), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewWithJavaScript(toLaunch); }), child: const Text('Launch in app(JavaScript ON)'), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewWithDomStorage(toLaunch); }), child: const Text('Launch in app(DOM storage ON)'), ), const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchUniversalLinkIos(toLaunch); }), diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml index 16448e4020e5..c7aac06b88e5 100644 --- a/packages/url_launcher/url_launcher_linux/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml @@ -1,6 +1,6 @@ name: url_launcher_linux description: Linux implementation of the url_launcher plugin. -version: 0.0.1+4 +version: 0.1.0-nullsafety.3 homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_linux flutter: @@ -10,8 +10,8 @@ flutter: pluginClass: UrlLauncherPlugin environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.12.8" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md index 190aa2887353..a5477a1b1501 100644 --- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md @@ -1,3 +1,23 @@ +# 0.1.0-nullsafety.2 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +# 0.1.0-nullsafety.1 + +* Bump SDK to support null safety. + +# 0.1.0-nullsafety + +* Migrate to null safety. + +## 0.0.2+1 + +* Update Flutter SDK constraint. + +## 0.0.2 + +* Update integration test examples to use `testWidgets` instead of `test`. + # 0.0.1+9 * Update Dart SDK constraint in example. diff --git a/packages/url_launcher/url_launcher_macos/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher_macos/example/integration_test/url_launcher_test.dart index 676b78c3bbfb..3e8d34c0b258 100644 --- a/packages/url_launcher/url_launcher_macos/example/integration_test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher_macos/example/integration_test/url_launcher_test.dart @@ -9,7 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - test('canLaunch', () async { + testWidgets('canLaunch', (WidgetTester _) async { expect(await canLaunch('randomstring'), false); // Generally all devices should have some default browser. diff --git a/packages/url_launcher/url_launcher_macos/example/lib/main.dart b/packages/url_launcher/url_launcher_macos/example/lib/main.dart index b5cce7482d07..a45862012328 100644 --- a/packages/url_launcher/url_launcher_macos/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_macos/example/lib/main.dart @@ -140,7 +140,7 @@ class _MyHomePageState extends State { decoration: const InputDecoration( hintText: 'Input the phone number to launch')), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _makePhoneCall('tel:$_phone'); }), @@ -150,33 +150,33 @@ class _MyHomePageState extends State { padding: EdgeInsets.all(16.0), child: Text(toLaunch), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInBrowser(toLaunch); }), child: const Text('Launch in browser'), ), const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewOrVC(toLaunch); }), child: const Text('Launch in app'), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewWithJavaScript(toLaunch); }), child: const Text('Launch in app(JavaScript ON)'), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewWithDomStorage(toLaunch); }), child: const Text('Launch in app(DOM storage ON)'), ), const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchUniversalLinkIos(toLaunch); }), diff --git a/packages/url_launcher/url_launcher_macos/pubspec.yaml b/packages/url_launcher/url_launcher_macos/pubspec.yaml index 1a77bc7aeab3..be2dd2739298 100644 --- a/packages/url_launcher/url_launcher_macos/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/pubspec.yaml @@ -3,7 +3,7 @@ description: macOS implementation of the url_launcher plugin. # 0.0.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.0.1+9 +version: 0.1.0-nullsafety.2 homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_macos flutter: @@ -14,8 +14,8 @@ flutter: fileName: url_launcher_macos.dart environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.12.8" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md index 10057e147adf..5bbbe9d28cd1 100644 --- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md @@ -1,3 +1,15 @@ +## 2.0.0-nullsafety.1 + +* Bump Dart SDK to support null safety. + +## 2.0.0-nullsafety + +* Migrate to null safety. + +## 1.0.10 + +* Update Flutter SDK constraint. + ## 1.0.9 * Laid the groundwork for introducing a Link widget. 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 425dc886d29f..a176972e06bd 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart @@ -16,7 +16,7 @@ typedef FollowLink = Future Function(); /// the widget tree under it. typedef LinkWidgetBuilder = Widget Function( BuildContext context, - FollowLink followLink, + FollowLink? followLink, ); /// Signature for a delegate function to build the [Link] widget. @@ -31,7 +31,7 @@ final MethodCodec _codec = const JSONMethodCodec(); class LinkTarget { /// Const private constructor with a [debugLabel] to allow the creation of /// multiple distinct const instances. - const LinkTarget._({this.debugLabel}); + const LinkTarget._({required this.debugLabel}); /// Used to distinguish multiple const instances of [LinkTarget]. final String debugLabel; @@ -64,7 +64,7 @@ abstract class LinkInfo { LinkWidgetBuilder get builder; /// The destination that this link leads to. - Uri get uri; + Uri? get uri; /// The target indicating where to open the link. LinkTarget get target; @@ -80,10 +80,14 @@ Future pushRouteNameToFramework( String routeName, { @visibleForTesting bool debugForceRouter = false, }) { + final PlatformMessageCallback? onPlatformMessage = window.onPlatformMessage; + if (onPlatformMessage == null) { + return Future.value(null); + } final Completer completer = Completer(); if (debugForceRouter || _hasRouter(context)) { SystemNavigator.routeInformationUpdated(location: routeName); - window.onPlatformMessage( + onPlatformMessage( 'flutter/navigation', _codec.encodeMethodCall( MethodCall('pushRouteInformation', { @@ -94,7 +98,7 @@ Future pushRouteNameToFramework( completer.complete, ); } else { - window.onPlatformMessage( + onPlatformMessage( 'flutter/navigation', _codec.encodeMethodCall(MethodCall('pushRoute', routeName)), completer.complete, diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart index ac5bfa230289..7b9dfc9cc5cf 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show required; import 'link.dart'; import 'url_launcher_platform_interface.dart'; @@ -15,14 +14,14 @@ const MethodChannel _channel = MethodChannel('plugins.flutter.io/url_launcher'); /// An implementation of [UrlLauncherPlatform] that uses method channels. class MethodChannelUrlLauncher extends UrlLauncherPlatform { @override - final LinkDelegate linkDelegate = null; + final LinkDelegate? linkDelegate = null; @override Future canLaunch(String url) { return _channel.invokeMethod( 'canLaunch', {'url': url}, - ); + ).then((value) => value ?? false); } @override @@ -33,13 +32,13 @@ class MethodChannelUrlLauncher extends UrlLauncherPlatform { @override Future launch( String url, { - @required bool useSafariVC, - @required bool useWebView, - @required bool enableJavaScript, - @required bool enableDomStorage, - @required bool universalLinksOnly, - @required Map headers, - String webOnlyWindowName, + required bool useSafariVC, + required bool useWebView, + required bool enableJavaScript, + required bool enableDomStorage, + required bool universalLinksOnly, + required Map headers, + String? webOnlyWindowName, }) { return _channel.invokeMethod( 'launch', @@ -52,6 +51,6 @@ class MethodChannelUrlLauncher extends UrlLauncherPlatform { 'universalLinksOnly': universalLinksOnly, 'headers': headers, }, - ); + ).then((value) => value ?? false); } } diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart index 75002ff9eb4d..2a4edfa8d1af 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart @@ -4,7 +4,6 @@ import 'dart:async'; -import 'package:meta/meta.dart' show required; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:url_launcher_platform_interface/link.dart'; @@ -40,7 +39,7 @@ abstract class UrlLauncherPlatform extends PlatformInterface { } /// The delegate used by the Link widget to build itself. - LinkDelegate get linkDelegate; + LinkDelegate? get linkDelegate; /// Returns `true` if this platform is able to launch [url]. Future canLaunch(String url) { @@ -53,13 +52,13 @@ abstract class UrlLauncherPlatform extends PlatformInterface { /// in `package:url_launcher/url_launcher.dart`. Future launch( String url, { - @required bool useSafariVC, - @required bool useWebView, - @required bool enableJavaScript, - @required bool enableDomStorage, - @required bool universalLinksOnly, - @required Map headers, - String webOnlyWindowName, + required bool useSafariVC, + required bool useWebView, + required bool enableJavaScript, + required bool enableDomStorage, + required bool universalLinksOnly, + required Map headers, + String? webOnlyWindowName, }) { throw UnimplementedError('launch() has not been implemented.'); } diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml index ce0fdd936c9a..e576e967ec46 100644 --- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml +++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml @@ -3,20 +3,19 @@ description: A common platform interface for the url_launcher plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_platform_interface # 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: 1.0.9 +version: 2.0.0-nullsafety.1 dependencies: flutter: sdk: flutter - meta: ^1.0.5 - plugin_platform_interface: ^1.0.1 + plugin_platform_interface: ^1.1.0-nullsafety.1 dev_dependencies: flutter_test: sdk: flutter mockito: ^4.1.1 - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.22.0 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.22.0" diff --git a/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart index 99a885ccc179..58cdd22dca02 100644 --- a/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart +++ b/packages/url_launcher/url_launcher_platform_interface/test/link_test.dart @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(egarciad): Remove once Mockito has been migrated to null safety. +// @dart = 2.9 + import 'dart:ui'; import 'package:mockito/mockito.dart'; diff --git a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart index d88f53ad58d0..dfd4b7380c3e 100644 --- a/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart +++ b/packages/url_launcher/url_launcher_platform_interface/test/method_channel_url_launcher_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(mvanbeusekom): Remove once Mockito is migrated to null safety. +// @dart = 2.9 import 'package:mockito/mockito.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -42,6 +44,10 @@ void main() { final List log = []; channel.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; }); final MethodChannelUrlLauncher launcher = MethodChannelUrlLauncher(); @@ -62,6 +68,12 @@ void main() { ); }); + test('canLaunch should return false if platform returns null', () async { + final canLaunch = await launcher.canLaunch('http://example.com/'); + + expect(canLaunch, false); + }); + test('launch', () async { await launcher.launch( 'http://example.com/', @@ -270,6 +282,20 @@ void main() { ); }); + test('launch should return false if platform returns null', () async { + final launched = await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + + expect(launched, false); + }); + test('closeWebView default behavior', () async { await launcher.closeWebView(); expect( diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index 093029104f8f..c8d52f5df13f 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.5+2 + +- Update Flutter SDK constraint. + # 0.1.5+1 - Substitute `undefined_prefixed_name: ignore` analyzer setting by a `dart:ui` shim with conditional exports. [Issue](https://github.com/flutter/flutter/issues/69309). @@ -37,7 +41,7 @@ # 0.1.2 -- Adds "tel" and "sms" support +- Adds "tel" and "sms" support # 0.1.1+6 diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index b40a8ea236cc..2d1b8af8e49f 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/u # 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.1.5+1 +version: 0.1.5+2 flutter: plugin: @@ -15,6 +15,12 @@ flutter: dependencies: url_launcher_platform_interface: ^1.0.9 + # TODO(mvanbeusekom): Update to use pub.dev once null safety version is published. + # url_launcher_platform_interface: + # git: + # url: https://github.com/flutter/plugins.git + # ref: nnbd + # path: packages/url_launcher/url_launcher_platform_interface flutter: sdk: flutter flutter_web_plugins: @@ -25,6 +31,9 @@ dev_dependencies: flutter_test: sdk: flutter url_launcher: ^5.2.5 + # TODO(mvanbeusekom): Update to use pub.dev once null safety version is published. + # url_launcher: + # path: ../url_launcher pedantic: ^1.8.0 mockito: ^4.1.1 integration_test: @@ -32,4 +41,4 @@ dev_dependencies: environment: sdk: ">=2.2.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + flutter: ">=1.10.0" diff --git a/packages/url_launcher/url_launcher_web/test/pubspec.yaml b/packages/url_launcher/url_launcher_web/test/pubspec.yaml index e755dff85004..b8c541f72986 100644 --- a/packages/url_launcher/url_launcher_web/test/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/test/pubspec.yaml @@ -2,7 +2,7 @@ name: regular_integration_tests publish_to: none environment: - sdk: ">=2.2.2 <3.0.0" + sdk: ">=2.10.0-56.0.dev <3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index 3bf040f964e6..a1998c92e2e2 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,3 +1,23 @@ +## 0.1.0-nullsafety.2 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.1.0-nullsafety.1 + +* Bump Dart SDK to support null safety. + +## 0.1.0-nullsafety + +* Migrate to null-safety. + +## 0.0.2+1 + +* Update Flutter SDK constraint. + +## 0.0.2 + +* Update integration test examples to use `testWidgets` instead of `test`. + ## 0.0.1+3 * Update Dart SDK constraint in example. diff --git a/packages/url_launcher/url_launcher_windows/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher_windows/example/integration_test/url_launcher_test.dart index 7a33e00fec0a..2617150348ee 100644 --- a/packages/url_launcher/url_launcher_windows/example/integration_test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher_windows/example/integration_test/url_launcher_test.dart @@ -9,7 +9,7 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - test('canLaunch', () async { + testWidgets('canLaunch', (WidgetTester _) async { UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; expect(await launcher.canLaunch('randomstring'), false); diff --git a/packages/url_launcher/url_launcher_windows/example/lib/main.dart b/packages/url_launcher/url_launcher_windows/example/lib/main.dart index db59af1e5b95..e6c9f477b5a4 100644 --- a/packages/url_launcher/url_launcher_windows/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_windows/example/lib/main.dart @@ -76,7 +76,7 @@ class _MyHomePageState extends State { padding: EdgeInsets.all(16.0), child: Text(toLaunch), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInBrowser(toLaunch); }), diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index dd7423d017f8..f7c96bf4a1bd 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -3,7 +3,7 @@ description: Windows implementation of the url_launcher plugin. # 0.0.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.0.1+3 +version: 0.1.0-nullsafety.2 homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_windows flutter: @@ -13,8 +13,8 @@ flutter: pluginClass: UrlLauncherPlugin environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.12.8" dependencies: flutter: diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 90082d8358e3..f9bae8dee6e1 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,39 @@ +## 2.0.0-nullsafety.7 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 2.0.0-nullsafety.6 + +* Fix `VideoPlayerValue toString()` test. + +## 2.0.0-nullsafety.5 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 2.0.0-nullsafety.4 + +* Fixed an issue where `isBuffering` was not updating on Android. + +## 2.0.0-nullsafety.3 + +* Dart null safety requires `2.12`. + +## 2.0.0-nullsafety.2 + +* Bump SDK version. + +## 2.0.0-nullsafety.1 + +* Merge master. + +## 2.0.0-nullsafety + +* Migration to null safety. + +## 1.0.2 + +* Update Flutter SDK constraint. + ## 1.0.1 * Android: Dispose video players when app is closed. diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index 5eff770b87a2..e64ce152f85b 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -1,6 +1,6 @@ # Video Player plugin for Flutter -[![pub package](https://img.shields.io/pub/v/video_player.svg)](https://pub.dartlang.org/packages/video_player) +[![pub package](https://img.shields.io/pub/v/video_player.svg)](https://pub.dev/packages/video_player) A Flutter plugin for iOS, Android and Web for playing back video on a Widget surface. @@ -12,7 +12,7 @@ A Flutter plugin for iOS, Android and Web for playing back video on a Widget sur ## Installation -First, add `video_player` as a [dependency in your pubspec.yaml file](https://flutter.io/using-packages/). +First, add `video_player` as a [dependency in your pubspec.yaml file](https://flutter.dev/using-packages/). ### iOS diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java index 78da7150edf0..98cf6dbaacea 100644 --- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v0.1.7), do not edit directly. +// Autogenerated from Pigeon (v0.1.12), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.videoplayer; @@ -597,7 +597,7 @@ static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { private static HashMap wrapError(Exception exception) { HashMap errorMap = new HashMap<>(); errorMap.put("message", exception.toString()); - errorMap.put("code", null); + errorMap.put("code", exception.getClass().getSimpleName()); errorMap.put("details", null); return errorMap; } diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 33c2f42afe1e..65657509b49f 100644 --- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -169,10 +169,21 @@ public void onCancel(Object o) { exoPlayer.addListener( new EventListener() { + private boolean isBuffering = false; + + public void setBuffering(boolean buffering) { + if (isBuffering != buffering) { + isBuffering = buffering; + Map event = new HashMap<>(); + event.put("event", isBuffering ? "bufferingStart" : "bufferingEnd"); + eventSink.success(event); + } + } @Override public void onPlaybackStateChanged(final int playbackState) { if (playbackState == Player.STATE_BUFFERING) { + setBuffering(true); sendBufferingUpdate(); } else if (playbackState == Player.STATE_READY) { if (!isInitialized) { @@ -184,10 +195,15 @@ public void onPlaybackStateChanged(final int playbackState) { event.put("event", "completed"); eventSink.success(event); } + + if (playbackState != Player.STATE_BUFFERING) { + setBuffering(false); + } } @Override public void onPlayerError(final ExoPlaybackException error) { + setBuffering(false); if (eventSink != null) { eventSink.error("VideoError", "Video player had error " + error, null); } diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart index 639cca9b8631..9e273e02dc4d 100644 --- a/packages/video_player/video_player/example/integration_test/video_player_test.dart +++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart @@ -2,6 +2,12 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// TODO(amirh): Remove this once flutter_driver supports null safety. +// https://github.com/flutter/flutter/issues/71379 +// @dart = 2.9 +import 'dart:async'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:integration_test/integration_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -22,17 +28,65 @@ void main() { testWidgets('can be initialized', (WidgetTester tester) async { await _controller.initialize(); - expect(_controller.value.initialized, true); + expect(_controller.value.isInitialized, true); expect(_controller.value.position, const Duration(seconds: 0)); expect(_controller.value.isPlaying, false); expect(_controller.value.duration, const Duration(seconds: 7, milliseconds: 540)); }); + testWidgets( + 'reports buffering status', + (WidgetTester tester) async { + VideoPlayerController networkController = VideoPlayerController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + ); + await networkController.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await networkController.setVolume(0); + final Completer started = Completer(); + final Completer ended = Completer(); + bool startedBuffering = false; + bool endedBuffering = false; + networkController.addListener(() { + if (networkController.value.isBuffering && !startedBuffering) { + startedBuffering = true; + started.complete(); + } + if (startedBuffering && + !networkController.value.isBuffering && + !endedBuffering) { + endedBuffering = true; + ended.complete(); + } + }); + + await networkController.play(); + await networkController.seekTo(const Duration(seconds: 5)); + await tester.pumpAndSettle(_playDuration); + await networkController.pause(); + + expect(networkController.value.isPlaying, false); + expect(networkController.value.position, + (Duration position) => position > const Duration(seconds: 0)); + + await started; + expect(startedBuffering, true); + + await ended; + expect(endedBuffering, true); + }, + skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), + ); + testWidgets( 'can be played', (WidgetTester tester) async { await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); await _controller.play(); await tester.pumpAndSettle(_playDuration); @@ -58,6 +112,9 @@ void main() { 'can be paused', (WidgetTester tester) async { await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); // Play for a second, then pause, and then wait a second. await _controller.play(); @@ -104,6 +161,6 @@ void main() { await tester.pumpAndSettle(); expect(_controller.value.isPlaying, true); - }); + }, skip: kIsWeb); // Web does not support local assets. }); } diff --git a/packages/video_player/video_player/example/lib/main.dart b/packages/video_player/video_player/example/lib/main.dart index a99b9da6bd0c..c874f740eab2 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -108,7 +108,7 @@ class _ButterFlyAssetVideoInList extends StatelessWidget { /// A filler card to show the video in a list of scrolling contents. class _ExampleCard extends StatelessWidget { - const _ExampleCard({Key key, this.title}) : super(key: key); + const _ExampleCard({Key? key, required this.title}) : super(key: key); final String title; @@ -124,13 +124,13 @@ class _ExampleCard extends StatelessWidget { ), ButtonBar( children: [ - FlatButton( + TextButton( child: const Text('BUY TICKETS'), onPressed: () { /* ... */ }, ), - FlatButton( + TextButton( child: const Text('SELL TICKETS'), onPressed: () { /* ... */ @@ -150,7 +150,7 @@ class _ButterFlyAssetVideo extends StatefulWidget { } class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> { - VideoPlayerController _controller; + late VideoPlayerController _controller; @override void initState() { @@ -206,7 +206,7 @@ class _BumbleBeeRemoteVideo extends StatefulWidget { } class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { - VideoPlayerController _controller; + late VideoPlayerController _controller; Future _loadCaptions() async { final String fileContents = await DefaultAssetBundle.of(context) @@ -265,7 +265,8 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { } class _ControlsOverlay extends StatelessWidget { - const _ControlsOverlay({Key key, this.controller}) : super(key: key); + const _ControlsOverlay({Key? key, required this.controller}) + : super(key: key); static const _examplePlaybackRates = [ 0.25, @@ -345,7 +346,7 @@ class _PlayerVideoAndPopPage extends StatefulWidget { } class _PlayerVideoAndPopPageState extends State<_PlayerVideoAndPopPage> { - VideoPlayerController _videoPlayerController; + late VideoPlayerController _videoPlayerController; bool startedPlaying = false; @override diff --git a/packages/video_player/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml index dd8fd8d06bd8..fb18d8b75efa 100644 --- a/packages/video_player/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -16,7 +16,8 @@ dev_dependencies: sdk: flutter integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + test: any + pedantic: ^1.10.0-nullsafety.1 flutter: uses-material-design: true @@ -26,5 +27,5 @@ flutter: - assets/bumble_bee_captions.srt environment: - sdk: ">=2.8.0 <3.0.0" + sdk: ">=2.12.0-0 <3.0.0" flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/packages/video_player/video_player/example/test_driver/integration_test.dart b/packages/video_player/video_player/example/test_driver/integration_test.dart index 7a2c21338786..7873abae2996 100644 --- a/packages/video_player/video_player/example/test_driver/integration_test.dart +++ b/packages/video_player/video_player/example/test_driver/integration_test.dart @@ -2,6 +2,9 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// TODO(egarciad): Remove once Flutter driver is migrated to null safety. +// @dart = 2.9 + import 'dart:async'; import 'dart:convert'; import 'dart:io'; diff --git a/packages/video_player/video_player/example/test_driver/video_player.dart b/packages/video_player/video_player/example/test_driver/video_player.dart new file mode 100644 index 000000000000..c1ced19e9b7e --- /dev/null +++ b/packages/video_player/video_player/example/test_driver/video_player.dart @@ -0,0 +1,14 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// TODO(egarciad): Remove once Flutter driver is migrated to null safety. +// @dart = 2.9 + +import 'package:flutter_driver/driver_extension.dart'; +import 'package:video_player_example/main.dart' as app; + +void main() { + enableFlutterDriverExtension(); + app.main(); +} diff --git a/packages/video_player/video_player/example/test_driver/video_player_test.dart b/packages/video_player/video_player/example/test_driver/video_player_test.dart new file mode 100644 index 000000000000..fcbdbb274f7a --- /dev/null +++ b/packages/video_player/video_player/example/test_driver/video_player_test.dart @@ -0,0 +1,30 @@ +// Copyright 2019, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +// TODO(egarciad): Remove once Flutter driver is migrated to null safety. +// @dart = 2.9 + +import 'dart:async'; +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart'; + +Future main() async { + final FlutterDriver driver = await FlutterDriver.connect(); + tearDownAll(() async { + await driver.close(); + }); + + //TODO(cyanglaz): Use TabBar tabs to navigate between pages after https://github.com/flutter/flutter/issues/16991 is fixed. + //TODO(cyanglaz): Un-skip the test after https://github.com/flutter/flutter/issues/43012 is fixed + test('Push a page contains video and pop back, do not crash.', () async { + final SerializableFinder pushTab = find.byValueKey('push_tab'); + await driver.waitFor(pushTab); + await driver.tap(pushTab); + await driver.waitForAbsent(pushTab); + await driver.waitFor(find.byValueKey('home_page')); + await driver.waitUntilNoTransientCallbacks(); + final Health health = await driver.checkHealth(); + expect(health.status, HealthStatus.ok); + }, skip: 'Cirrus CI currently hangs while playing videos'); +} diff --git a/packages/video_player/video_player/ios/Classes/messages.h b/packages/video_player/video_player/ios/Classes/messages.h index 3c68b3dd24d4..84e8fc5e5cff 100644 --- a/packages/video_player/video_player/ios/Classes/messages.h +++ b/packages/video_player/video_player/ios/Classes/messages.h @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v0.1.7), do not edit directly. +// Autogenerated from Pigeon (v0.1.12), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @protocol FlutterBinaryMessenger; diff --git a/packages/video_player/video_player/ios/Classes/messages.m b/packages/video_player/video_player/ios/Classes/messages.m index e71f8b89254d..58ff7292d2b2 100644 --- a/packages/video_player/video_player/ios/Classes/messages.m +++ b/packages/video_player/video_player/ios/Classes/messages.m @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v0.1.7), do not edit directly. +// Autogenerated from Pigeon (v0.1.12), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.h" #import @@ -7,6 +7,7 @@ #error File requires ARC to be enabled. #endif +#ifndef __clang_analyzer__ static NSDictionary *wrapResult(NSDictionary *result, FlutterError *error) { NSDictionary *errorDict = (NSDictionary *)[NSNull null]; if (error) { @@ -59,9 +60,9 @@ + (FLTTextureMessage *)fromMap:(NSDictionary *)dict { return result; } - (NSDictionary *)toMap { - return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", nil]; + return + [NSDictionary dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), + @"textureId", nil]; } @end @@ -112,10 +113,9 @@ + (FLTLoopingMessage *)fromMap:(NSDictionary *)dict { } - (NSDictionary *)toMap { return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", - (self.isLooping != nil ? self.isLooping : [NSNull null]), - @"isLooping", nil]; + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.isLooping ? self.isLooping : [NSNull null]), @"isLooping", + nil]; } @end @@ -134,9 +134,8 @@ + (FLTVolumeMessage *)fromMap:(NSDictionary *)dict { } - (NSDictionary *)toMap { return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", (self.volume != nil ? self.volume : [NSNull null]), - @"volume", nil]; + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.volume ? self.volume : [NSNull null]), @"volume", nil]; } @end @@ -155,9 +154,8 @@ + (FLTPlaybackSpeedMessage *)fromMap:(NSDictionary *)dict { } - (NSDictionary *)toMap { return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", (self.speed != nil ? self.speed : [NSNull null]), - @"speed", nil]; + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.speed ? self.speed : [NSNull null]), @"speed", nil]; } @end @@ -176,10 +174,9 @@ + (FLTPositionMessage *)fromMap:(NSDictionary *)dict { } - (NSDictionary *)toMap { return [NSDictionary - dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), - @"textureId", - (self.position != nil ? self.position : [NSNull null]), - @"position", nil]; + dictionaryWithObjectsAndKeys:(self.textureId ? self.textureId : [NSNull null]), @"textureId", + (self.position ? self.position : [NSNull null]), @"position", + nil]; } @end @@ -194,7 +191,7 @@ + (FLTMixWithOthersMessage *)fromMap:(NSDictionary *)dict { } - (NSDictionary *)toMap { return [NSDictionary - dictionaryWithObjectsAndKeys:(self.mixWithOthers != nil ? self.mixWithOthers : [NSNull null]), + dictionaryWithObjectsAndKeys:(self.mixWithOthers ? self.mixWithOthers : [NSNull null]), @"mixWithOthers", nil]; } @end @@ -365,3 +362,4 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, id _parseCaptionsFromSubRipString(String file) { end: startAndEnd.end, text: text, ); - - if (newCaption.start != null && newCaption.end != null) { + if (newCaption.start != newCaption.end) { captions.add(newCaption); } } @@ -64,7 +63,7 @@ class _StartAndEnd { RegExp(_subRipTimeStamp + _subRipArrow + _subRipTimeStamp); if (!format.hasMatch(line)) { - return _StartAndEnd(null, null); + return _StartAndEnd(Duration.zero, Duration.zero); } final List times = line.split(_subRipArrow); @@ -84,7 +83,7 @@ class _StartAndEnd { // Duration(hours: 0, minutes: 1, seconds: 59, milliseconds: 084) Duration _parseSubRipTimestamp(String timestampString) { if (!RegExp(_subRipTimeStamp).hasMatch(timestampString)) { - return null; + return Duration.zero; } final List commaSections = timestampString.split(','); diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index ac1645085e36..6a2af76fa547 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -28,11 +28,12 @@ class VideoPlayerValue { /// Constructs a video with the given values. Only [duration] is required. The /// rest will initialize with default values when unset. VideoPlayerValue({ - @required this.duration, - this.size, - this.position = const Duration(), - this.caption = const Caption(), + required this.duration, + this.size = Size.zero, + this.position = Duration.zero, + this.caption = Caption.none, this.buffered = const [], + this.isInitialized = false, this.isPlaying = false, this.isLooping = false, this.isBuffering = false, @@ -41,17 +42,20 @@ class VideoPlayerValue { this.errorDescription, }); - /// Returns an instance with a `null` [Duration]. - VideoPlayerValue.uninitialized() : this(duration: null); + /// Returns an instance for a video that hasn't been loaded. + VideoPlayerValue.uninitialized() + : this(duration: Duration.zero, isInitialized: false); - /// Returns an instance with a `null` [Duration] and the given - /// [errorDescription]. + /// Returns an instance with the given [errorDescription]. VideoPlayerValue.erroneous(String errorDescription) - : this(duration: null, errorDescription: errorDescription); + : this( + duration: Duration.zero, + isInitialized: false, + errorDescription: errorDescription); /// The total duration of the video. /// - /// Is null when [initialized] is false. + /// The duration is [Duration.zero] if the video hasn't been initialized. final Duration duration; /// The current playback position. @@ -60,7 +64,7 @@ class VideoPlayerValue { /// The [Caption] that should be displayed based on the current [position]. /// /// This field will never be null. If there is no caption for the current - /// [position], this will be an empty [Caption] object. + /// [position], this will be a [Caption.none] object. final Caption caption; /// The currently buffered ranges. @@ -84,7 +88,7 @@ class VideoPlayerValue { /// A description of the error if present. /// /// If [hasError] is false this is [null]. - final String errorDescription; + final String? errorDescription; /// The [size] of the currently loaded video. /// @@ -92,7 +96,7 @@ class VideoPlayerValue { final Size size; /// Indicates whether or not the video has been loaded and is ready to play. - bool get initialized => duration != null; + final bool isInitialized; /// Indicates whether or not the video is in an error state. If this is true /// [errorDescription] should have information about the problem. @@ -101,7 +105,7 @@ class VideoPlayerValue { /// Returns [size.width] / [size.height] when size is non-null, or `1.0.` when /// size is null or the aspect ratio would be less than or equal to 0.0. double get aspectRatio { - if (size == null || size.width == 0 || size.height == 0) { + if (!isInitialized || size.width == 0 || size.height == 0) { return 1.0; } final double aspectRatio = size.width / size.height; @@ -114,17 +118,18 @@ class VideoPlayerValue { /// Returns a new instance that has the same values as this current instance, /// except for any overrides passed in as arguments to [copyWidth]. VideoPlayerValue copyWith({ - Duration duration, - Size size, - Duration position, - Caption caption, - List buffered, - bool isPlaying, - bool isLooping, - bool isBuffering, - double volume, - double playbackSpeed, - String errorDescription, + Duration? duration, + Size? size, + Duration? position, + Caption? caption, + List? buffered, + bool? isInitialized, + bool? isPlaying, + bool? isLooping, + bool? isBuffering, + double? volume, + double? playbackSpeed, + String? errorDescription, }) { return VideoPlayerValue( duration: duration ?? this.duration, @@ -132,6 +137,7 @@ class VideoPlayerValue { position: position ?? this.position, caption: caption ?? this.caption, buffered: buffered ?? this.buffered, + isInitialized: isInitialized ?? this.isInitialized, isPlaying: isPlaying ?? this.isPlaying, isLooping: isLooping ?? this.isLooping, isBuffering: isBuffering ?? this.isBuffering, @@ -149,6 +155,7 @@ class VideoPlayerValue { 'position: $position, ' 'caption: $caption, ' 'buffered: [${buffered.join(', ')}], ' + 'isInitialized: $isInitialized, ' 'isPlaying: $isPlaying, ' 'isLooping: $isLooping, ' 'isBuffering: $isBuffering, ' @@ -178,7 +185,7 @@ class VideoPlayerController extends ValueNotifier { {this.package, this.closedCaptionFile, this.videoPlayerOptions}) : dataSourceType = DataSourceType.asset, formatHint = null, - super(VideoPlayerValue(duration: null)); + super(VideoPlayerValue(duration: Duration.zero)); /// Constructs a [VideoPlayerController] playing a video from obtained from /// the network. @@ -191,7 +198,7 @@ class VideoPlayerController extends ValueNotifier { {this.formatHint, this.closedCaptionFile, this.videoPlayerOptions}) : dataSourceType = DataSourceType.network, package = null, - super(VideoPlayerValue(duration: null)); + super(VideoPlayerValue(duration: Duration.zero)); /// Constructs a [VideoPlayerController] playing a video from a file. /// @@ -203,9 +210,7 @@ class VideoPlayerController extends ValueNotifier { dataSourceType = DataSourceType.file, package = null, formatHint = null, - super(VideoPlayerValue(duration: null)); - - int _textureId; + super(VideoPlayerValue(duration: Duration.zero)); /// The URI to the video file. This will be in different formats depending on /// the [DataSourceType] of the original video. @@ -213,31 +218,36 @@ class VideoPlayerController extends ValueNotifier { /// **Android only**. Will override the platform's generic file format /// detection with whatever is set here. - final VideoFormat formatHint; + final VideoFormat? formatHint; /// Describes the type of data source this [VideoPlayerController] /// is constructed with. final DataSourceType dataSourceType; /// Provide additional configuration options (optional). Like setting the audio mode to mix - final VideoPlayerOptions videoPlayerOptions; + final VideoPlayerOptions? videoPlayerOptions; /// Only set for [asset] videos. The package that the asset was loaded from. - final String package; + final String? package; /// Optional field to specify a file containing the closed /// captioning. /// /// This future will be awaited and the file will be loaded when /// [initialize()] is called. - final Future closedCaptionFile; + final Future? closedCaptionFile; - ClosedCaptionFile _closedCaptionFile; - Timer _timer; + ClosedCaptionFile? _closedCaptionFile; + Timer? _timer; bool _isDisposed = false; - Completer _creatingCompleter; - StreamSubscription _eventSubscription; - _VideoAppLifeCycleObserver _lifeCycleObserver; + Completer? _creatingCompleter; + StreamSubscription? _eventSubscription; + late _VideoAppLifeCycleObserver _lifeCycleObserver; + + /// The id of a texture that hasn't been initialized. + @visibleForTesting + static const int kUninitializedTextureId = -1; + int _textureId = kUninitializedTextureId; /// This is just exposed for testing. It shouldn't be used by anyone depending /// on the plugin. @@ -250,7 +260,7 @@ class VideoPlayerController extends ValueNotifier { _lifeCycleObserver.initialize(); _creatingCompleter = Completer(); - DataSource dataSourceDescription; + late DataSource dataSourceDescription; switch (dataSourceType) { case DataSourceType.asset: dataSourceDescription = DataSource( @@ -276,11 +286,12 @@ class VideoPlayerController extends ValueNotifier { if (videoPlayerOptions?.mixWithOthers != null) { await _videoPlayerPlatform - .setMixWithOthers(videoPlayerOptions.mixWithOthers); + .setMixWithOthers(videoPlayerOptions!.mixWithOthers); } - _textureId = await _videoPlayerPlatform.create(dataSourceDescription); - _creatingCompleter.complete(null); + _textureId = (await _videoPlayerPlatform.create(dataSourceDescription)) ?? + kUninitializedTextureId; + _creatingCompleter!.complete(null); final Completer initializingCompleter = Completer(); void eventListener(VideoEvent event) { @@ -293,6 +304,7 @@ class VideoPlayerController extends ValueNotifier { value = value.copyWith( duration: event.duration, size: event.size, + isInitialized: event.duration != null, ); initializingCompleter.complete(null); _applyLooping(); @@ -325,8 +337,8 @@ class VideoPlayerController extends ValueNotifier { } void errorListener(Object obj) { - final PlatformException e = obj; - value = VideoPlayerValue.erroneous(e.message); + final PlatformException e = obj as PlatformException; + value = VideoPlayerValue.erroneous(e.message!); _timer?.cancel(); if (!initializingCompleter.isCompleted) { initializingCompleter.completeError(obj); @@ -342,7 +354,7 @@ class VideoPlayerController extends ValueNotifier { @override Future dispose() async { if (_creatingCompleter != null) { - await _creatingCompleter.future; + await _creatingCompleter!.future; if (!_isDisposed) { _isDisposed = true; _timer?.cancel(); @@ -379,14 +391,14 @@ class VideoPlayerController extends ValueNotifier { } Future _applyLooping() async { - if (!value.initialized || _isDisposed) { + if (!value.isInitialized || _isDisposed) { return; } await _videoPlayerPlatform.setLooping(_textureId, value.isLooping); } Future _applyPlayPause() async { - if (!value.initialized || _isDisposed) { + if (!value.isInitialized || _isDisposed) { return; } if (value.isPlaying) { @@ -400,8 +412,8 @@ class VideoPlayerController extends ValueNotifier { if (_isDisposed) { return; } - final Duration newPosition = await position; - if (_isDisposed) { + final Duration? newPosition = await position; + if (newPosition == null) { return; } _updatePosition(newPosition); @@ -419,14 +431,14 @@ class VideoPlayerController extends ValueNotifier { } Future _applyVolume() async { - if (!value.initialized || _isDisposed) { + if (!value.isInitialized || _isDisposed) { return; } await _videoPlayerPlatform.setVolume(_textureId, value.volume); } Future _applyPlaybackSpeed() async { - if (!value.initialized || _isDisposed) { + if (!value.isInitialized || _isDisposed) { return; } @@ -442,7 +454,7 @@ class VideoPlayerController extends ValueNotifier { } /// The position in the current video. - Future get position async { + Future get position async { if (_isDisposed) { return null; } @@ -519,17 +531,17 @@ class VideoPlayerController extends ValueNotifier { /// [Caption]. Caption _getCaptionAt(Duration position) { if (_closedCaptionFile == null) { - return Caption(); + return Caption.none; } // TODO: This would be more efficient as a binary search. - for (final caption in _closedCaptionFile.captions) { + for (final caption in _closedCaptionFile!.captions) { if (caption.start <= position && caption.end >= position) { return caption; } } - return Caption(); + return Caption.none; } void _updatePosition(Duration position) { @@ -545,7 +557,7 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { final VideoPlayerController _controller; void initialize() { - WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance!.addObserver(this); } @override @@ -565,7 +577,7 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { } void dispose() { - WidgetsBinding.instance.removeObserver(this); + WidgetsBinding.instance!.removeObserver(this); } } @@ -594,8 +606,9 @@ class _VideoPlayerState extends State { }; } - VoidCallback _listener; - int _textureId; + late VoidCallback _listener; + + late int _textureId; @override void initState() { @@ -622,7 +635,7 @@ class _VideoPlayerState extends State { @override Widget build(BuildContext context) { - return _textureId == null + return _textureId == VideoPlayerController.kUninitializedTextureId ? Container() : _videoPlayerPlatform.buildView(_textureId); } @@ -646,7 +659,7 @@ class VideoProgressColors { /// [backgroundColor] defaults to gray at 50% opacity. This is the background /// color behind both [playedColor] and [bufferedColor] to denote the total /// size of the video compared to either of those values. - VideoProgressColors({ + const VideoProgressColors({ this.playedColor = const Color.fromRGBO(255, 0, 0, 0.7), this.bufferedColor = const Color.fromRGBO(50, 50, 200, 0.2), this.backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5), @@ -670,8 +683,8 @@ class VideoProgressColors { class _VideoScrubber extends StatefulWidget { _VideoScrubber({ - @required this.child, - @required this.controller, + required this.child, + required this.controller, }); final Widget child; @@ -689,7 +702,7 @@ class _VideoScrubberState extends State<_VideoScrubber> { @override Widget build(BuildContext context) { void seekToRelativePosition(Offset globalPosition) { - final RenderBox box = context.findRenderObject(); + final RenderBox box = context.findRenderObject() as RenderBox; final Offset tapPos = box.globalToLocal(globalPosition); final double relative = tapPos.dx / box.size.width; final Duration position = controller.value.duration * relative; @@ -700,7 +713,7 @@ class _VideoScrubberState extends State<_VideoScrubber> { behavior: HitTestBehavior.opaque, child: widget.child, onHorizontalDragStart: (DragStartDetails details) { - if (!controller.value.initialized) { + if (!controller.value.isInitialized) { return; } _controllerWasPlaying = controller.value.isPlaying; @@ -709,7 +722,7 @@ class _VideoScrubberState extends State<_VideoScrubber> { } }, onHorizontalDragUpdate: (DragUpdateDetails details) { - if (!controller.value.initialized) { + if (!controller.value.isInitialized) { return; } seekToRelativePosition(details.globalPosition); @@ -720,7 +733,7 @@ class _VideoScrubberState extends State<_VideoScrubber> { } }, onTapDown: (TapDownDetails details) { - if (!controller.value.initialized) { + if (!controller.value.isInitialized) { return; } seekToRelativePosition(details.globalPosition); @@ -745,10 +758,10 @@ class VideoProgressIndicator extends StatefulWidget { /// to `top: 5.0`. VideoProgressIndicator( this.controller, { - VideoProgressColors colors, - this.allowScrubbing, + this.colors = const VideoProgressColors(), + required this.allowScrubbing, this.padding = const EdgeInsets.only(top: 5.0), - }) : colors = colors ?? VideoProgressColors(); + }); /// The [VideoPlayerController] that actually associates a video with this /// widget. @@ -785,7 +798,7 @@ class _VideoProgressIndicatorState extends State { }; } - VoidCallback listener; + late VoidCallback listener; VideoPlayerController get controller => widget.controller; @@ -806,7 +819,7 @@ class _VideoProgressIndicatorState extends State { @override Widget build(BuildContext context) { Widget progressIndicator; - if (controller.value.initialized) { + if (controller.value.isInitialized) { final int duration = controller.value.duration.inMilliseconds; final int position = controller.value.position.inMilliseconds; @@ -878,17 +891,17 @@ class ClosedCaption extends StatelessWidget { /// [VideoPlayerValue.caption]. /// /// If [text] is null, nothing will be displayed. - const ClosedCaption({Key key, this.text, this.textStyle}) : super(key: key); + const ClosedCaption({Key? key, this.text, this.textStyle}) : super(key: key); /// The text that will be shown in the closed caption, or null if no caption /// should be shown. - final String text; + final String? text; /// Specifies how the text in the closed caption should look. /// /// If null, defaults to [DefaultTextStyle.of(context).style] with size 36 /// font colored white. - final TextStyle textStyle; + final TextStyle? textStyle; @override Widget build(BuildContext context) { @@ -913,7 +926,7 @@ class ClosedCaption extends StatelessWidget { ), child: Padding( padding: EdgeInsets.symmetric(horizontal: 2.0), - child: Text(text, style: effectiveTextStyle), + child: Text(text!, style: effectiveTextStyle), ), ), ), diff --git a/packages/video_player/video_player/pigeons/messages.dart b/packages/video_player/video_player/pigeons/messages.dart index 427aea279071..f1771afecb45 100644 --- a/packages/video_player/video_player/pigeons/messages.dart +++ b/packages/video_player/video_player/pigeons/messages.dart @@ -1,3 +1,5 @@ +// @dart = 2.9 + import 'package:pigeon/pigeon_lib.dart'; class TextureMessage { diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 69be8b24100b..e4694195ebde 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -1,7 +1,10 @@ name: video_player description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. -version: 1.0.1 +# 0.10.y+z is compatible with 1.0.0, if you land a breaking change bump +# the version to 2.0.0. +# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 +version: 2.0.0-nullsafety.7 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player flutter: @@ -16,15 +19,15 @@ flutter: default_package: video_player_web dependencies: - meta: ^1.0.5 - video_player_platform_interface: ^2.2.0 + meta: ^1.3.0-nullsafety.3 + video_player_platform_interface: ^3.0.0-nullsafety.3 # The design on https://flutter.dev/go/federated-plugins was to leave # this constraint as "any". We cannot do it right now as it fails pub publish # validation, so we set a ^ constraint. # TODO(amirh): Revisit this (either update this part in the design or the pub tool). # https://github.com/flutter/flutter/issues/46264 - video_player_web: '>=0.1.4 <2.0.0' + video_player_web: ^2.0.0-nullsafety.1 flutter: sdk: flutter @@ -32,9 +35,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 pigeon: 0.1.7 environment: - sdk: ">=2.8.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/video_player/video_player/test/sub_rip_file_test.dart b/packages/video_player/video_player/test/sub_rip_file_test.dart index cf25ff73e438..2b9803d8275e 100644 --- a/packages/video_player/video_player/test/sub_rip_file_test.dart +++ b/packages/video_player/video_player/test/sub_rip_file_test.dart @@ -108,6 +108,6 @@ This one is valid 3 00:01:54,724 --> 00:01:6,760 -This one should be ignored because the +This one should be ignored because the ned time is missing a digit. '''; diff --git a/packages/video_player/video_player/test/video_player_initialization_test.dart b/packages/video_player/video_player/test/video_player_initialization_test.dart index 231c399bb8fe..1a09ed9f718c 100644 --- a/packages/video_player/video_player/test/video_player_initialization_test.dart +++ b/packages/video_player/video_player/test/video_player_initialization_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 - import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player/video_player.dart'; 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 a7b23c3b1f27..f3f2b5e8faf1 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -2,8 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// @dart = 2.8 - import 'dart:async'; import 'dart:io'; @@ -18,7 +16,7 @@ import 'package:video_player_platform_interface/video_player_platform_interface. class FakeController extends ValueNotifier implements VideoPlayerController { - FakeController() : super(VideoPlayerValue(duration: null)); + FakeController() : super(VideoPlayerValue(duration: Duration.zero)); @override Future dispose() async { @@ -26,7 +24,7 @@ class FakeController extends ValueNotifier } @override - int textureId; + int textureId = VideoPlayerController.kUninitializedTextureId; @override String get dataSource => ''; @@ -35,7 +33,7 @@ class FakeController extends ValueNotifier DataSourceType get dataSourceType => DataSourceType.file; @override - String get package => null; + String get package => ''; @override Future get position async => value.position; @@ -62,13 +60,13 @@ class FakeController extends ValueNotifier Future setLooping(bool looping) async {} @override - VideoFormat get formatHint => null; + VideoFormat? get formatHint => null; @override Future get closedCaptionFile => _loadClosedCaption(); @override - VideoPlayerOptions get videoPlayerOptions => null; + VideoPlayerOptions? get videoPlayerOptions => null; } Future _loadClosedCaption() async => @@ -80,11 +78,13 @@ class _FakeClosedCaptionFile extends ClosedCaptionFile { return [ Caption( text: 'one', + number: 0, start: Duration(milliseconds: 100), end: Duration(milliseconds: 200), ), Caption( text: 'two', + number: 1, start: Duration(milliseconds: 300), end: Duration(milliseconds: 400), ), @@ -101,6 +101,7 @@ void main() { controller.textureId = 123; controller.value = controller.value.copyWith( duration: const Duration(milliseconds: 100), + isInitialized: true, ); await tester.pump(); @@ -133,8 +134,8 @@ void main() { await tester.pumpWidget(MaterialApp(home: ClosedCaption(text: text))); final Text textWidget = tester.widget(find.text(text)); - expect(textWidget.style.fontSize, 36.0); - expect(textWidget.style.color, Colors.white); + expect(textWidget.style!.fontSize, 36.0); + expect(textWidget.style!.color, Colors.white); }); testWidgets('uses given text and style', (WidgetTester tester) async { @@ -149,7 +150,7 @@ void main() { expect(find.text(text), findsOneWidget); final Text textWidget = tester.widget(find.text(text)); - expect(textWidget.style.fontSize, textStyle.fontSize); + expect(textWidget.style!.fontSize, textStyle.fontSize); }); testWidgets('handles null text', (WidgetTester tester) async { @@ -173,7 +174,7 @@ void main() { }); group('VideoPlayerController', () { - FakeVideoPlayerPlatform fakeVideoPlayerPlatform; + late FakeVideoPlayerPlatform fakeVideoPlayerPlatform; setUp(() { fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); @@ -221,7 +222,7 @@ void main() { 'http://testing.com/invalid_url', ); try { - dynamic error; + late dynamic error; fakeVideoPlayerPlatform.forceInitError = true; await controller.initialize().catchError((dynamic e) => error = e); final PlatformException platformEx = error; @@ -245,13 +246,14 @@ void main() { final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', ); - expect(controller.textureId, isNull); + expect( + controller.textureId, VideoPlayerController.kUninitializedTextureId); expect(await controller.position, const Duration(seconds: 0)); await controller.initialize(); await controller.dispose(); - expect(controller.textureId, isNotNull); + expect(controller.textureId, 0); expect(await controller.position, isNull); }); @@ -390,19 +392,19 @@ void main() { await controller.initialize(); expect(controller.value.position, const Duration()); - expect(controller.value.caption.text, isNull); + expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 100)); expect(controller.value.caption.text, 'one'); await controller.seekTo(const Duration(milliseconds: 250)); - expect(controller.value.caption.text, isNull); + expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'two'); await controller.seekTo(const Duration(milliseconds: 500)); - expect(controller.value.caption.text, isNull); + expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'two'); @@ -419,8 +421,7 @@ void main() { await controller.play(); expect(controller.value.isPlaying, isTrue); final FakeVideoEventStream fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]; - assert(fakeVideoEventStream != null); + fakeVideoPlayerPlatform.streams[controller.textureId]!; fakeVideoEventStream.eventsChannel .sendEvent({'event': 'completed'}); @@ -438,8 +439,7 @@ void main() { expect(controller.value.isBuffering, false); expect(controller.value.buffered, isEmpty); final FakeVideoEventStream fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]; - assert(fakeVideoEventStream != null); + fakeVideoPlayerPlatform.streams[controller.textureId]!; fakeVideoEventStream.eventsChannel .sendEvent({'event': 'bufferingStart'}); @@ -496,9 +496,9 @@ void main() { test('uninitialized()', () { final VideoPlayerValue uninitialized = VideoPlayerValue.uninitialized(); - expect(uninitialized.duration, isNull); - expect(uninitialized.position, equals(const Duration(seconds: 0))); - expect(uninitialized.caption, equals(const Caption())); + expect(uninitialized.duration, equals(Duration.zero)); + expect(uninitialized.position, equals(Duration.zero)); + expect(uninitialized.caption, equals(Caption.none)); expect(uninitialized.buffered, isEmpty); expect(uninitialized.isPlaying, isFalse); expect(uninitialized.isLooping, isFalse); @@ -506,9 +506,8 @@ void main() { expect(uninitialized.volume, 1.0); expect(uninitialized.playbackSpeed, 1.0); expect(uninitialized.errorDescription, isNull); - expect(uninitialized.size, isNull); - expect(uninitialized.size, isNull); - expect(uninitialized.initialized, isFalse); + expect(uninitialized.size, equals(Size.zero)); + expect(uninitialized.isInitialized, isFalse); expect(uninitialized.hasError, isFalse); expect(uninitialized.aspectRatio, 1.0); }); @@ -517,9 +516,9 @@ void main() { const String errorMessage = 'foo'; final VideoPlayerValue error = VideoPlayerValue.erroneous(errorMessage); - expect(error.duration, isNull); - expect(error.position, equals(const Duration(seconds: 0))); - expect(error.caption, equals(const Caption())); + expect(error.duration, equals(Duration.zero)); + expect(error.position, equals(Duration.zero)); + expect(error.caption, equals(Caption.none)); expect(error.buffered, isEmpty); expect(error.isPlaying, isFalse); expect(error.isLooping, isFalse); @@ -527,9 +526,8 @@ void main() { expect(error.volume, 1.0); expect(error.playbackSpeed, 1.0); expect(error.errorDescription, errorMessage); - expect(error.size, isNull); - expect(error.size, isNull); - expect(error.initialized, isFalse); + expect(error.size, equals(Size.zero)); + expect(error.isInitialized, isFalse); expect(error.hasError, isTrue); expect(error.aspectRatio, 1.0); }); @@ -538,10 +536,12 @@ void main() { const Duration duration = Duration(seconds: 5); const Size size = Size(400, 300); const Duration position = Duration(seconds: 1); - const Caption caption = Caption(text: 'foo'); + const Caption caption = Caption( + text: 'foo', number: 0, start: Duration.zero, end: Duration.zero); final List buffered = [ DurationRange(const Duration(seconds: 0), const Duration(seconds: 4)) ]; + const bool isInitialized = true; const bool isPlaying = true; const bool isLooping = true; const bool isBuffering = true; @@ -554,6 +554,7 @@ void main() { position: position, caption: caption, buffered: buffered, + isInitialized: isInitialized, isPlaying: isPlaying, isLooping: isLooping, isBuffering: isBuffering, @@ -566,8 +567,9 @@ void main() { 'VideoPlayerValue(duration: 0:00:05.000000, ' 'size: Size(400.0, 300.0), ' 'position: 0:00:01.000000, ' - 'caption: Caption(number: null, start: null, end: null, text: foo), ' + 'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), ' 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' + 'isInitialized: true, ' 'isPlaying: true, ' 'isLooping: true, ' 'isBuffering: true, ' @@ -586,15 +588,16 @@ void main() { group('aspectRatio', () { test('640x480 -> 4:3', () { final value = VideoPlayerValue( + isInitialized: true, size: Size(640, 480), duration: Duration(seconds: 1), ); expect(value.aspectRatio, 4 / 3); }); - test('null size -> 1.0', () { + test('no size -> 1.0', () { final value = VideoPlayerValue( - size: null, + isInitialized: true, duration: Duration(seconds: 1), ); expect(value.aspectRatio, 1.0); @@ -602,6 +605,7 @@ void main() { test('height = 0 -> 1.0', () { final value = VideoPlayerValue( + isInitialized: true, size: Size(640, 0), duration: Duration(seconds: 1), ); @@ -610,6 +614,7 @@ void main() { test('width = 0 -> 1.0', () { final value = VideoPlayerValue( + isInitialized: true, size: Size(0, 480), duration: Duration(seconds: 1), ); @@ -618,6 +623,7 @@ void main() { test('negative aspect ratio -> 1.0', () { final value = VideoPlayerValue( + isInitialized: true, size: Size(640, -480), duration: Duration(seconds: 1), ); @@ -646,7 +652,7 @@ void main() { File(''), videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true)); await controller.initialize(); - expect(controller.videoPlayerOptions.mixWithOthers, true); + expect(controller.videoPlayerOptions!.mixWithOthers, true); }); } @@ -706,7 +712,7 @@ class FakeVideoPlayerPlatform extends TestHostVideoPlayerApi { @override void seekTo(PositionMessage arg) { calls.add('seekTo'); - _positions[arg.textureId] = Duration(milliseconds: arg.position); + _positions[arg.textureId!] = Duration(milliseconds: arg.position!); } @override @@ -742,7 +748,7 @@ class FakeVideoEventStream { int height; Duration duration; bool initWithError; - FakeEventsChannel eventsChannel; + late FakeEventsChannel eventsChannel; void onListen() { if (!initWithError) { @@ -764,7 +770,7 @@ class FakeEventsChannel { eventsMethodChannel.setMockMethodCallHandler(onMethodCall); } - MethodChannel eventsMethodChannel; + late MethodChannel eventsMethodChannel; VoidCallback onListen; Future onMethodCall(MethodCall call) { @@ -780,7 +786,7 @@ class FakeEventsChannel { _sendMessage(const StandardMethodCodec().encodeSuccessEnvelope(event)); } - void sendError(String code, [String message, dynamic details]) { + void sendError(String code, [String? message, dynamic details]) { _sendMessage(const StandardMethodCodec().encodeErrorEnvelope( code: code, message: message, @@ -794,6 +800,6 @@ class FakeEventsChannel { // available on all the versions of Flutter that we test. // ignore: deprecated_member_use defaultBinaryMessenger.handlePlatformMessage( - eventsMethodChannel.name, data, (ByteData data) {}); + eventsMethodChannel.name, data, (ByteData? data) {}); } } diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 8af22f783675..446fffd9a60e 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,23 @@ +## 3.0.0-nullsafety.3 + +* `messages.dart` sets Dart `2.12`. + +## 3.0.0-nullsafety.2 + +* Bump Dart SDK to support null safety. + +## 3.0.0-nullsafety.1 + +* Make DataSource's `uri` parameter nullable. + +## 3.0.0-nullsafety + +* Migrate to null safety. + +## 2.2.1 + +* Update Flutter SDK constraint. + ## 2.2.0 * Added option to set the video playback speed on the video controller. diff --git a/packages/video_player/video_player_platform_interface/lib/messages.dart b/packages/video_player/video_player_platform_interface/lib/messages.dart index bfe65f1fd2ea..252cad6993ca 100644 --- a/packages/video_player/video_player_platform_interface/lib/messages.dart +++ b/packages/video_player/video_player_platform_interface/lib/messages.dart @@ -1,13 +1,13 @@ -// Autogenerated from Pigeon (v0.1.7), do not edit directly. +// Autogenerated from Pigeon (v0.1.12), 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 -// @dart = 2.8 +// @dart = 2.12 import 'dart:async'; import 'package:flutter/services.dart'; import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; class TextureMessage { - int textureId; + int? textureId; // ignore: unused_element Map _toMap() { final Map pigeonMap = {}; @@ -17,9 +17,6 @@ class TextureMessage { // ignore: unused_element static TextureMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } final TextureMessage result = TextureMessage(); result.textureId = pigeonMap['textureId']; return result; @@ -27,10 +24,10 @@ class TextureMessage { } class CreateMessage { - String asset; - String uri; - String packageName; - String formatHint; + String? asset; + String? uri; + String? packageName; + String? formatHint; // ignore: unused_element Map _toMap() { final Map pigeonMap = {}; @@ -43,9 +40,6 @@ class CreateMessage { // ignore: unused_element static CreateMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } final CreateMessage result = CreateMessage(); result.asset = pigeonMap['asset']; result.uri = pigeonMap['uri']; @@ -56,8 +50,8 @@ class CreateMessage { } class LoopingMessage { - int textureId; - bool isLooping; + int? textureId; + bool? isLooping; // ignore: unused_element Map _toMap() { final Map pigeonMap = {}; @@ -68,9 +62,6 @@ class LoopingMessage { // ignore: unused_element static LoopingMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } final LoopingMessage result = LoopingMessage(); result.textureId = pigeonMap['textureId']; result.isLooping = pigeonMap['isLooping']; @@ -79,8 +70,8 @@ class LoopingMessage { } class VolumeMessage { - int textureId; - double volume; + int? textureId; + double? volume; // ignore: unused_element Map _toMap() { final Map pigeonMap = {}; @@ -91,9 +82,6 @@ class VolumeMessage { // ignore: unused_element static VolumeMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } final VolumeMessage result = VolumeMessage(); result.textureId = pigeonMap['textureId']; result.volume = pigeonMap['volume']; @@ -102,8 +90,8 @@ class VolumeMessage { } class PlaybackSpeedMessage { - int textureId; - double speed; + int? textureId; + double? speed; // ignore: unused_element Map _toMap() { final Map pigeonMap = {}; @@ -114,9 +102,6 @@ class PlaybackSpeedMessage { // ignore: unused_element static PlaybackSpeedMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } final PlaybackSpeedMessage result = PlaybackSpeedMessage(); result.textureId = pigeonMap['textureId']; result.speed = pigeonMap['speed']; @@ -125,8 +110,8 @@ class PlaybackSpeedMessage { } class PositionMessage { - int textureId; - int position; + int? textureId; + int? position; // ignore: unused_element Map _toMap() { final Map pigeonMap = {}; @@ -137,9 +122,6 @@ class PositionMessage { // ignore: unused_element static PositionMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } final PositionMessage result = PositionMessage(); result.textureId = pigeonMap['textureId']; result.position = pigeonMap['position']; @@ -148,7 +130,7 @@ class PositionMessage { } class MixWithOthersMessage { - bool mixWithOthers; + bool? mixWithOthers; // ignore: unused_element Map _toMap() { final Map pigeonMap = {}; @@ -158,9 +140,6 @@ class MixWithOthersMessage { // ignore: unused_element static MixWithOthersMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } final MixWithOthersMessage result = MixWithOthersMessage(); result.mixWithOthers = pigeonMap['mixWithOthers']; return result; @@ -172,7 +151,7 @@ class VideoPlayerApi { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.initialize', StandardMessageCodec()); - final Map replyMap = await channel.send(null); + final Map? replyMap = await channel.send(null); if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -194,7 +173,7 @@ class VideoPlayerApi { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.create', StandardMessageCodec()); - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = await channel.send(requestMap); if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -216,7 +195,7 @@ class VideoPlayerApi { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.dispose', StandardMessageCodec()); - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = await channel.send(requestMap); if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -238,7 +217,7 @@ class VideoPlayerApi { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setLooping', StandardMessageCodec()); - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = await channel.send(requestMap); if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -260,7 +239,7 @@ class VideoPlayerApi { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setVolume', StandardMessageCodec()); - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = await channel.send(requestMap); if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -283,7 +262,7 @@ class VideoPlayerApi { 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', StandardMessageCodec()); - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = await channel.send(requestMap); if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -305,7 +284,7 @@ class VideoPlayerApi { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.play', StandardMessageCodec()); - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = await channel.send(requestMap); if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -327,7 +306,7 @@ class VideoPlayerApi { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = await channel.send(requestMap); if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -349,7 +328,7 @@ class VideoPlayerApi { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.seekTo', StandardMessageCodec()); - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = await channel.send(requestMap); if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -371,7 +350,7 @@ class VideoPlayerApi { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.pause', StandardMessageCodec()); - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = await channel.send(requestMap); if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -394,7 +373,7 @@ class VideoPlayerApi { 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', StandardMessageCodec()); - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = await channel.send(requestMap); if (replyMap == null) { throw PlatformException( code: 'channel-error', @@ -424,131 +403,175 @@ abstract class TestHostVideoPlayerApi { void seekTo(PositionMessage arg); void pause(TextureMessage arg); void setMixWithOthers(MixWithOthersMessage arg); - static void setup(TestHostVideoPlayerApi api) { + static void setup(TestHostVideoPlayerApi? api) { { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.initialize', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - api.initialize(); - return {}; - }); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((dynamic message) async { + api.initialize(); + return {}; + }); + } } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.create', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final CreateMessage input = CreateMessage._fromMap(mapMessage); - final TextureMessage output = api.create(input); - return {'result': output._toMap()}; - }); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final CreateMessage input = CreateMessage._fromMap(mapMessage); + final TextureMessage output = api.create(input); + return {'result': output._toMap()}; + }); + } } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.dispose', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - api.dispose(input); - return {}; - }); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final TextureMessage input = TextureMessage._fromMap(mapMessage); + api.dispose(input); + return {}; + }); + } } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setLooping', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final LoopingMessage input = LoopingMessage._fromMap(mapMessage); - api.setLooping(input); - return {}; - }); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final LoopingMessage input = LoopingMessage._fromMap(mapMessage); + api.setLooping(input); + return {}; + }); + } } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setVolume', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final VolumeMessage input = VolumeMessage._fromMap(mapMessage); - api.setVolume(input); - return {}; - }); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final VolumeMessage input = VolumeMessage._fromMap(mapMessage); + api.setVolume(input); + return {}; + }); + } } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final PlaybackSpeedMessage input = - PlaybackSpeedMessage._fromMap(mapMessage); - api.setPlaybackSpeed(input); - return {}; - }); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final PlaybackSpeedMessage input = + PlaybackSpeedMessage._fromMap(mapMessage); + api.setPlaybackSpeed(input); + return {}; + }); + } } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.play', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - api.play(input); - return {}; - }); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final TextureMessage input = TextureMessage._fromMap(mapMessage); + api.play(input); + return {}; + }); + } } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - final PositionMessage output = api.position(input); - return {'result': output._toMap()}; - }); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final TextureMessage input = TextureMessage._fromMap(mapMessage); + final PositionMessage output = api.position(input); + return {'result': output._toMap()}; + }); + } } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.seekTo', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final PositionMessage input = PositionMessage._fromMap(mapMessage); - api.seekTo(input); - return {}; - }); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final PositionMessage input = PositionMessage._fromMap(mapMessage); + api.seekTo(input); + return {}; + }); + } } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.pause', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - api.pause(input); - return {}; - }); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final TextureMessage input = TextureMessage._fromMap(mapMessage); + api.pause(input); + return {}; + }); + } } { const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final MixWithOthersMessage input = - MixWithOthersMessage._fromMap(mapMessage); - api.setMixWithOthers(input); - return {}; - }); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = + message as Map; + final MixWithOthersMessage input = + MixWithOthersMessage._fromMap(mapMessage); + api.setMixWithOthers(input); + return {}; + }); + } } } } diff --git a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart index 0ea443fb6e12..9b007d00d6a9 100644 --- a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart +++ b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart @@ -26,7 +26,7 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { } @override - Future create(DataSource dataSource) async { + Future create(DataSource dataSource) async { CreateMessage message = CreateMessage(); switch (dataSource.sourceType) { @@ -91,7 +91,7 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { Future getPosition(int textureId) async { PositionMessage response = await _api.position(TextureMessage()..textureId = textureId); - return Duration(milliseconds: response.position); + return Duration(milliseconds: response.position!); } @override diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 2757fb135af6..f2bc00205acc 100644 --- a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart +++ b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart @@ -7,7 +7,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart' show required, visibleForTesting; +import 'package:meta/meta.dart' show visibleForTesting; import 'method_channel_video_player.dart'; @@ -66,7 +66,7 @@ abstract class VideoPlayerPlatform { } /// Creates an instance of a video player and returns its textureId. - Future create(DataSource dataSource) { + Future create(DataSource dataSource) { throw UnimplementedError('create() has not been implemented.'); } @@ -146,7 +146,7 @@ class DataSource { /// The [package] argument must be non-null when the asset comes from a /// package and null otherwise. DataSource({ - @required this.sourceType, + required this.sourceType, this.uri, this.formatHint, this.asset, @@ -163,18 +163,18 @@ class DataSource { /// /// This will be in different formats depending on the [DataSourceType] of /// the original video. - final String uri; + final String? uri; /// **Android only**. Will override the platform's generic file format /// detection with whatever is set here. - final VideoFormat formatHint; + final VideoFormat? formatHint; /// The name of the asset. Only set for [DataSourceType.asset] videos. - final String asset; + final String? asset; /// The package that the asset was loaded from. Only set for /// [DataSourceType.asset] videos. - final String package; + final String? package; } /// The way in which the video was originally loaded. @@ -216,7 +216,7 @@ class VideoEvent { /// Depending on the [eventType], the [duration], [size] and [buffered] /// arguments can be null. VideoEvent({ - @required this.eventType, + required this.eventType, this.duration, this.size, this.buffered, @@ -228,17 +228,17 @@ class VideoEvent { /// Duration of the video. /// /// Only used if [eventType] is [VideoEventType.initialized]. - final Duration duration; + final Duration? duration; /// Size of the video. /// /// Only used if [eventType] is [VideoEventType.initialized]. - final Size size; + final Size? size; /// Buffered parts of the video. /// /// Only used if [eventType] is [VideoEventType.bufferingUpdate]. - final List buffered; + final List? buffered; @override bool operator ==(Object other) { diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index cc3cd79f1f33..ea8d3179cf1d 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -3,19 +3,19 @@ description: A common platform interface for the video_player plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_platform_interface # 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.2.0 +version: 3.0.0-nullsafety.3 dependencies: flutter: sdk: flutter - meta: ^1.0.5 + meta: ^1.3.0-nullsafety.3 dev_dependencies: flutter_test: sdk: flutter mockito: ^4.1.1 - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 environment: - sdk: ">=2.8.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.10.0" diff --git a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart index c4791001ad92..5c19ebca0d12 100644 --- a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart +++ b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// TODO(egarciad): Remove once Mockito is migrated to null safety. +// @dart = 2.9 + import 'dart:ui'; import 'package:flutter/services.dart'; diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index d8bebd3ae4b6..2becf5b85e0a 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.0.0-nullsafety.2 + +* Fixed an issue where `isBuffering` was not updating on Web. + +## 2.0.0-nullsafety.1 + +* Bump Dart SDK to support null safety. + +## 2.0.0-nullsafety + +* Migrate to null safety. + +## 0.1.4+2 + +* Update Flutter SDK constraint. + ## 0.1.4+1 * Substitute `undefined_prefixed_name: ignore` analyzer setting by a `dart:ui` shim with conditional exports. [Issue](https://github.com/flutter/flutter/issues/69309). diff --git a/packages/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart index 6715d5aca53b..9132a08437da 100644 --- a/packages/video_player/video_player_web/lib/video_player_web.dart +++ b/packages/video_player/video_player_web/lib/video_player_web.dart @@ -50,7 +50,7 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { @override Future dispose(int textureId) async { - _videoPlayers[textureId].dispose(); + _videoPlayers[textureId]!.dispose(); _videoPlayers.remove(textureId); return null; } @@ -66,16 +66,16 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { final int textureId = _textureCounter; _textureCounter++; - String uri; + late String uri; switch (dataSource.sourceType) { case DataSourceType.network: // Do NOT modify the incoming uri, it can be a Blob, and Safari doesn't // like blobs that have changed. - uri = dataSource.uri; + uri = dataSource.uri ?? ''; break; case DataSourceType.asset: - String assetUrl = dataSource.asset; - if (dataSource.package != null && dataSource.package.isNotEmpty) { + String assetUrl = dataSource.asset!; + if (dataSource.package != null && dataSource.package!.isNotEmpty) { assetUrl = 'packages/${dataSource.package}/$assetUrl'; } assetUrl = ui.webOnlyAssetManager.getAssetUrl(assetUrl); @@ -99,45 +99,45 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { @override Future setLooping(int textureId, bool looping) async { - return _videoPlayers[textureId].setLooping(looping); + return _videoPlayers[textureId]!.setLooping(looping); } @override Future play(int textureId) async { - return _videoPlayers[textureId].play(); + return _videoPlayers[textureId]!.play(); } @override Future pause(int textureId) async { - return _videoPlayers[textureId].pause(); + return _videoPlayers[textureId]!.pause(); } @override Future setVolume(int textureId, double volume) async { - return _videoPlayers[textureId].setVolume(volume); + return _videoPlayers[textureId]!.setVolume(volume); } @override Future setPlaybackSpeed(int textureId, double speed) async { assert(speed > 0); - return _videoPlayers[textureId].setPlaybackSpeed(speed); + return _videoPlayers[textureId]!.setPlaybackSpeed(speed); } @override Future seekTo(int textureId, Duration position) async { - return _videoPlayers[textureId].seekTo(position); + return _videoPlayers[textureId]!.seekTo(position); } @override Future getPosition(int textureId) async { - _videoPlayers[textureId].sendBufferingUpdate(); - return _videoPlayers[textureId].getPosition(); + _videoPlayers[textureId]!.sendBufferingUpdate(); + return _videoPlayers[textureId]!.getPosition(); } @override Stream videoEventsFor(int textureId) { - return _videoPlayers[textureId].eventController.stream; + return _videoPlayers[textureId]!.eventController.stream; } @override @@ -147,15 +147,26 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { } class _VideoPlayer { - _VideoPlayer({this.uri, this.textureId}); + _VideoPlayer({required this.uri, required this.textureId}); final StreamController eventController = StreamController(); final String uri; final int textureId; - VideoElement videoElement; + late VideoElement videoElement; bool isInitialized = false; + bool isBuffering = false; + + void setBuffering(bool buffering) { + if (isBuffering != buffering) { + isBuffering = buffering; + eventController.add(VideoEvent( + eventType: isBuffering + ? VideoEventType.bufferingStart + : VideoEventType.bufferingEnd)); + } + } void initialize() { videoElement = VideoElement() @@ -176,22 +187,38 @@ class _VideoPlayer { isInitialized = true; sendInitialized(); } + setBuffering(false); + }); + + videoElement.onCanPlayThrough.listen((dynamic _) { + setBuffering(false); + }); + + videoElement.onPlaying.listen((dynamic _) { + setBuffering(false); + }); + + videoElement.onWaiting.listen((dynamic _) { + setBuffering(true); + sendBufferingUpdate(); }); // The error event fires when some form of error occurs while attempting to load or perform the media. videoElement.onError.listen((Event _) { + setBuffering(false); // The Event itself (_) doesn't contain info about the actual error. // We need to look at the HTMLMediaElement.error. // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error - MediaError error = videoElement.error; + MediaError error = videoElement.error!; eventController.addError(PlatformException( - code: _kErrorValueToErrorName[error.code], + code: _kErrorValueToErrorName[error.code]!, message: error.message != '' ? error.message : _kDefaultErrorMessage, details: _kErrorValueToErrorDescription[error.code], )); }); videoElement.onEnded.listen((dynamic _) { + setBuffering(false); eventController.add(VideoEvent(eventType: VideoEventType.completed)); }); } @@ -258,8 +285,8 @@ class _VideoPlayer { milliseconds: (videoElement.duration * 1000).round(), ), size: Size( - videoElement.videoWidth.toDouble() ?? 0.0, - videoElement.videoHeight.toDouble() ?? 0.0, + videoElement.videoWidth.toDouble(), + videoElement.videoHeight.toDouble(), ), ), ); diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index ae05bfbcf910..9333ac0ac6f0 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/v # 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.1.4+1 +version: 2.0.0-nullsafety.2 flutter: plugin: @@ -18,16 +18,16 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 - video_player_platform_interface: ^2.2.0 + meta: ^1.3.0-nullsafety.3 + video_player_platform_interface: ^3.0.0-nullsafety.3 dev_dependencies: flutter_test: sdk: flutter video_player: path: ../video_player - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 environment: - sdk: ">=2.8.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.12.8" diff --git a/packages/video_player/video_player_web/test/video_player_web_test.dart b/packages/video_player/video_player_web/test/video_player_web_test.dart index 453079bfcd40..c433d82027f0 100644 --- a/packages/video_player/video_player_web/test/video_player_web_test.dart +++ b/packages/video_player/video_player_web/test/video_player_web_test.dart @@ -14,16 +14,16 @@ import 'package:video_player_web/video_player_web.dart'; void main() { group('VideoPlayer for Web', () { - int textureId; + late int textureId; setUp(() async { VideoPlayerPlatform.instance = VideoPlayerPlugin(); - textureId = await VideoPlayerPlatform.instance.create( + textureId = (await VideoPlayerPlatform.instance.create( DataSource( sourceType: DataSourceType.network, uri: 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'), - ); + ))!; }); test('$VideoPlayerPlugin is the live instance', () { @@ -84,12 +84,12 @@ void main() { }); test('throws PlatformException when playing bad media', () async { - int videoPlayerId = await VideoPlayerPlatform.instance.create( + int videoPlayerId = (await VideoPlayerPlatform.instance.create( DataSource( sourceType: DataSourceType.network, uri: 'https://flutter.github.io/assets-for-api-docs/assets/videos/_non_existent_video.mp4'), - ); + ))!; Stream eventStream = VideoPlayerPlatform.instance.videoEventsFor(videoPlayerId); diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 69706e1f2cb5..f1e39fcc4f90 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,23 @@ +## 2.0.0-nullsafety.3 + +* Fix `onWebResourceError` on iOS. + +## 2.0.0-nullsafety.2 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 2.0.0-nullsafety.1 + +* Added `allowsInlineMediaPlayback` property. + +## 2.0.0-nullsafety + +* Migration to null-safety. + +## 1.0.8 + +* Update Flutter SDK constraint. + ## 1.0.7 * Minor documentation update to indicate known issue on iOS 13.4 and 13.5. diff --git a/packages/webview_flutter/README.md b/packages/webview_flutter/README.md index 6852ebb34522..df554eb5123c 100644 --- a/packages/webview_flutter/README.md +++ b/packages/webview_flutter/README.md @@ -1,6 +1,6 @@ # WebView for Flutter -[![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dartlang.org/packages/webview_flutter) +[![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dev/packages/webview_flutter) A Flutter plugin that provides a WebView widget. @@ -8,7 +8,7 @@ On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com On Android the WebView widget is backed by a [WebView](https://developer.android.com/reference/android/webkit/WebView). ## Usage -Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). You can now include a WebView widget in your widget tree. See the [WebView](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebView-class.html) diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index bfb79a39e8ba..ef9f006f6e5b 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -372,6 +372,9 @@ private void applySettings(Map settings) { case "userAgent": updateUserAgent((String) settings.get(key)); break; + case "allowsInlineMediaPlayback": + // no-op inline media playback is always allowed on Android. + break; default: throw new IllegalArgumentException("Unknown WebView setting: " + key); } diff --git a/packages/webview_flutter/example/assets/sample_video.mp4 b/packages/webview_flutter/example/assets/sample_video.mp4 new file mode 100644 index 000000000000..a203d0cdf13e Binary files /dev/null and b/packages/webview_flutter/example/assets/sample_video.mp4 differ diff --git a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart index 2a17c53a4c3f..5f39cc3d86d2 100644 --- a/packages/webview_flutter/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// @dart = 2.9 + import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -120,16 +122,14 @@ void main() { controllerCompleter.complete(controller); }, javascriptMode: JavascriptMode.unrestricted, - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ + javascriptChannels: { JavascriptChannel( name: 'Echo', onMessageReceived: (JavascriptMessage message) { messagesReceived.add(message.message); }, ), - ].toSet(), + }, onPageStarted: (String url) { pageStarted.complete(null); }, @@ -180,16 +180,14 @@ void main() { onWebViewCreated: (WebViewController controller) { controllerCompleter.complete(controller); }, - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ + javascriptChannels: { JavascriptChannel( name: 'Resize', onMessageReceived: (JavascriptMessage message) { resizeCompleter.complete(true); }, ), - ].toSet(), + }, onPageStarted: (String url) { pageStarted.complete(null); }, @@ -327,7 +325,220 @@ void main() { expect(customUserAgent2, defaultPlatformUserAgent); }); - group('Media playback policy', () { + group('Video playback policy', () { + String videoTestBase64; + setUpAll(() async { + final ByteData videoData = + await rootBundle.load('assets/sample_video.mp4'); + final String base64VideoData = + base64Encode(Uint8List.view(videoData.buffer)); + final String videoTest = ''' + + Video auto play + + + + + + + '''; + videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }, skip: true /* https://github.com/flutter/flutter/issues/72572 */); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }, skip: true /* https://github.com/flutter/flutter/issues/72572 */); + + testWidgets('Video plays inline when allowsInlineMediaPlayback is true', + (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: true, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isFullScreen = + await controller.evaluateJavascript('isFullScreen();'); + expect(isFullScreen, _webviewBool(false)); + + controllerCompleter = Completer(); + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: false, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageLoaded.future; + + isFullScreen = await controller.evaluateJavascript('isFullScreen();'); + expect(isFullScreen, _webviewBool(true)); + }, skip: true /* https://github.com/flutter/flutter/issues/72572 */); + }); + + group('Audio playback policy', () { String audioTestBase64; setUpAll(() async { final ByteData audioData = @@ -424,7 +635,7 @@ void main() { isPaused = await controller.evaluateJavascript('isPaused();'); expect(isPaused, _webviewBool(true)); - }); + }, skip: true /* https://github.com/flutter/flutter/issues/72572 */); testWidgets('Changes to initialMediaPlaybackPolocy are ignored', (WidgetTester tester) async { @@ -493,7 +704,7 @@ void main() { isPaused = await controller.evaluateJavascript('isPaused();'); expect(isPaused, _webviewBool(false)); - }); + }, skip: true /* https://github.com/flutter/flutter/issues/72572 */); }); testWidgets('getTitle', (WidgetTester tester) async { diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart index 7ec3008337d8..c7f42ac2bf66 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -62,11 +62,9 @@ class _WebViewExampleState extends State { onWebViewCreated: (WebViewController webViewController) { _controller.complete(webViewController); }, - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ + javascriptChannels: { _toasterJavascriptChannel(context), - ].toSet(), + }, navigationDelegate: (NavigationRequest request) { if (request.url.startsWith('https://www.youtube.com/')) { print('blocking navigation to $request}'); @@ -107,7 +105,7 @@ class _WebViewExampleState extends State { if (controller.hasData) { return FloatingActionButton( onPressed: () async { - final String url = await controller.data.currentUrl(); + final String url = (await controller.data!.currentUrl())!; // ignore: deprecated_member_use Scaffold.of(context).showSnackBar( SnackBar(content: Text('Favorited $url')), @@ -147,25 +145,25 @@ class SampleMenu extends StatelessWidget { onSelected: (MenuOptions value) { switch (value) { case MenuOptions.showUserAgent: - _onShowUserAgent(controller.data, context); + _onShowUserAgent(controller.data!, context); break; case MenuOptions.listCookies: - _onListCookies(controller.data, context); + _onListCookies(controller.data!, context); break; case MenuOptions.clearCookies: _onClearCookies(context); break; case MenuOptions.addToCache: - _onAddToCache(controller.data, context); + _onAddToCache(controller.data!, context); break; case MenuOptions.listCache: - _onListCache(controller.data, context); + _onListCache(controller.data!, context); break; case MenuOptions.clearCache: - _onClearCache(controller.data, context); + _onClearCache(controller.data!, context); break; case MenuOptions.navigationDelegate: - _onNavigationDelegateExample(controller.data, context); + _onNavigationDelegateExample(controller.data!, context); break; } }, @@ -301,7 +299,7 @@ class NavigationControls extends StatelessWidget { (BuildContext context, AsyncSnapshot snapshot) { final bool webViewReady = snapshot.connectionState == ConnectionState.done; - final WebViewController controller = snapshot.data; + final WebViewController controller = snapshot.data!; return Row( children: [ IconButton( diff --git a/packages/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/example/pubspec.yaml index 44c740ae9739..1ace8afe400f 100644 --- a/packages/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/example/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_example description: Demonstrates how to use the webview_flutter plugin. environment: - sdk: ">=2.0.0-dev.68.0 <3.0.0" + sdk: ">=2.12.0-0 <3.0.0" dependencies: flutter: @@ -23,3 +23,4 @@ flutter: uses-material-design: true assets: - assets/sample_audio.ogg + - assets/sample_video.mp4 diff --git a/packages/webview_flutter/example/test_driver/integration_test.dart b/packages/webview_flutter/example/test_driver/integration_test.dart index 7a2c21338786..a8a56aa90f6a 100644 --- a/packages/webview_flutter/example/test_driver/integration_test.dart +++ b/packages/webview_flutter/example/test_driver/integration_test.dart @@ -2,6 +2,8 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +// @dart = 2.9 + import 'dart:async'; import 'dart:convert'; import 'dart:io'; diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m index 5499e90b05e4..3c75cee1fe3c 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m @@ -335,6 +335,9 @@ - (NSString*)applySettings:(NSDictionary*)settings { } else if ([key isEqualToString:@"userAgent"]) { NSString* userAgent = settings[key]; [self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent]; + } else if ([key isEqualToString:@"allowsInlineMediaPlayback"]) { + NSNumber* allowsInlineMediaPlayback = settings[key]; + _webView.configuration.allowsInlineMediaPlayback = [allowsInlineMediaPlayback boolValue]; } else { [unknownKeys addObject:key]; } diff --git a/packages/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart index 1f95bac2907d..099017fae410 100644 --- a/packages/webview_flutter/lib/platform_interface.dart +++ b/packages/webview_flutter/lib/platform_interface.dart @@ -21,7 +21,8 @@ abstract class WebViewPlatformCallbacksHandler { /// Invoked by [WebViewPlatformController] when a navigation request is pending. /// /// If true is returned the navigation is allowed, otherwise it is blocked. - FutureOr onNavigationRequest({String url, bool isForMainFrame}); + FutureOr onNavigationRequest( + {required String url, required bool isForMainFrame}); /// Invoked by [WebViewPlatformController] when a page has started loading. void onPageStarted(String url); @@ -106,8 +107,8 @@ class WebResourceError { /// A user should not need to instantiate this class, but will receive one in /// [WebResourceErrorCallback]. WebResourceError({ - @required this.errorCode, - @required this.description, + required this.errorCode, + required this.description, this.domain, this.errorType, this.failingUrl, @@ -134,7 +135,7 @@ class WebResourceError { /// in Objective-C. See /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html /// for more information on error handling on iOS. - final String domain; + final String? domain; /// Description of the error that can be used to communicate the problem to the user. final String description; @@ -142,13 +143,13 @@ class WebResourceError { /// The type this error can be categorized as. /// /// This will never be `null` on Android, but can be `null` on iOS. - final WebResourceErrorType errorType; + final WebResourceErrorType? errorType; /// Gets the URL for which the resource request was made. /// /// This value is not provided on iOS. Alternatively, you can keep track of /// the last values provided to [WebViewPlatformController.loadUrl]. - final String failingUrl; + final String? failingUrl; } /// Interface for talking to the webview's platform implementation. @@ -179,7 +180,7 @@ abstract class WebViewPlatformController { /// Throws an ArgumentError if `url` is not a valid URL string. Future loadUrl( String url, - Map headers, + Map? headers, ) { throw UnimplementedError( "WebView loadUrl is not implemented on the current platform"); @@ -197,7 +198,7 @@ abstract class WebViewPlatformController { /// Accessor to the current URL that the WebView is displaying. /// /// If no URL was ever loaded, returns `null`. - Future currentUrl() { + Future currentUrl() { throw UnimplementedError( "WebView currentUrl is not implemented on the current platform"); } @@ -284,7 +285,7 @@ abstract class WebViewPlatformController { } /// Returns the title of the currently loaded page. - Future getTitle() { + Future getTitle() { throw UnimplementedError( "WebView getTitle is not implemented on the current platform"); } @@ -340,7 +341,7 @@ class WebSetting { : _value = value, isPresent = true; - final T _value; + final T? _value; /// The setting's value. /// @@ -350,7 +351,14 @@ class WebSetting { throw StateError('Cannot access a value of an absent WebSetting'); } assert(isPresent); - return _value; + // The intention of this getter is to return T whether it is nullable or + // not whereas _value is of type T? since _value can be null even when + // T is not nullable (when isPresent == false). + // + // We promote _value to T using `as T` instead of `!` operator to handle + // the case when _value is legitimately null (and T is a nullable type). + // `!` operator would always throw if _value is null. + return _value as T; } /// True when this web setting instance contains a value. @@ -361,7 +369,7 @@ class WebSetting { @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - final WebSetting typedOther = other; + final WebSetting typedOther = other as WebSetting; return typedOther.isPresent == isPresent && typedOther._value == _value; } @@ -385,19 +393,25 @@ class WebSettings { this.hasNavigationDelegate, this.debuggingEnabled, this.gestureNavigationEnabled, - @required this.userAgent, + this.allowsInlineMediaPlayback, + required this.userAgent, }) : assert(userAgent != null); /// The JavaScript execution mode to be used by the webview. - final JavascriptMode javascriptMode; + final JavascriptMode? javascriptMode; /// Whether the [WebView] has a [NavigationDelegate] set. - final bool hasNavigationDelegate; + final bool? hasNavigationDelegate; /// Whether to enable the platform's webview content debugging tools. /// /// See also: [WebView.debuggingEnabled]. - final bool debuggingEnabled; + final bool? debuggingEnabled; + + /// Whether to play HTML5 videos inline or use the native full-screen controller on iOS. + /// + /// This will have no effect on Android. + final bool? allowsInlineMediaPlayback; /// The value used for the HTTP `User-Agent:` request header. /// @@ -407,16 +421,16 @@ class WebSettings { /// last time it was set. /// /// See also [WebView.userAgent]. - final WebSetting userAgent; + final WebSetting userAgent; /// Whether to allow swipe based navigation in iOS. /// /// See also: [WebView.gestureNavigationEnabled] - final bool gestureNavigationEnabled; + final bool? gestureNavigationEnabled; @override String toString() { - return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent)'; + return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)'; } } @@ -431,7 +445,7 @@ class CreationParams { CreationParams({ this.initialUrl, this.webSettings, - this.javascriptChannelNames, + this.javascriptChannelNames = const {}, this.userAgent, this.autoMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, @@ -440,12 +454,12 @@ class CreationParams { /// The initialUrl to load in the webview. /// /// When null the webview will be created without loading any page. - final String initialUrl; + final String? initialUrl; /// The initial [WebSettings] for the new webview. /// /// This can later be updated with [WebViewPlatformController.updateSettings]. - final WebSettings webSettings; + final WebSettings? webSettings; /// The initial set of JavaScript channels that are configured for this webview. /// @@ -463,7 +477,7 @@ class CreationParams { /// The value used for the HTTP User-Agent: request header. /// /// When null the platform's webview default is used for the User-Agent header. - final String userAgent; + final String? userAgent; /// Which restrictions apply on automatic media playback. final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy; @@ -478,7 +492,7 @@ class CreationParams { /// /// See also the `onWebViewPlatformCreated` argument for [WebViewPlatform.build]. typedef WebViewPlatformCreatedCallback = void Function( - WebViewPlatformController webViewPlatformController); + WebViewPlatformController? webViewPlatformController); /// Interface for a platform implementation of a WebView. /// @@ -508,14 +522,14 @@ abstract class WebViewPlatform { /// /// `webViewPlatformHandler` must not be null. Widget build({ - BuildContext context, + required BuildContext context, // TODO(amirh): convert this to be the actual parameters. // I'm starting without it as the PR is starting to become pretty big. // I'll followup with the conversion PR. - CreationParams creationParams, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, }); /// Clears all cookies for all [WebView] instances. diff --git a/packages/webview_flutter/lib/src/webview_android.dart b/packages/webview_flutter/lib/src/webview_android.dart index f7afcc0637a3..cba9e1b698b8 100644 --- a/packages/webview_flutter/lib/src/webview_android.dart +++ b/packages/webview_flutter/lib/src/webview_android.dart @@ -20,11 +20,11 @@ import 'webview_method_channel.dart'; class AndroidWebView implements WebViewPlatform { @override Widget build({ - BuildContext context, - CreationParams creationParams, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, }) { assert(webViewPlatformCallbacksHandler != null); return GestureDetector( diff --git a/packages/webview_flutter/lib/src/webview_cupertino.dart b/packages/webview_flutter/lib/src/webview_cupertino.dart index 0e84908261e4..e6816555f73b 100644 --- a/packages/webview_flutter/lib/src/webview_cupertino.dart +++ b/packages/webview_flutter/lib/src/webview_cupertino.dart @@ -20,11 +20,11 @@ import 'webview_method_channel.dart'; class CupertinoWebView implements WebViewPlatform { @override Widget build({ - BuildContext context, - CreationParams creationParams, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, }) { return UiKitView( viewType: 'plugins.flutter.io/webview', diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart index c2a83420f28c..54ab647cdc04 100644 --- a/packages/webview_flutter/lib/src/webview_method_channel.dart +++ b/packages/webview_flutter/lib/src/webview_method_channel.dart @@ -25,34 +25,32 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { static const MethodChannel _cookieManagerChannel = MethodChannel('plugins.flutter.io/cookie_manager'); - Future _onMethodCall(MethodCall call) async { + Future _onMethodCall(MethodCall call) async { switch (call.method) { case 'javascriptChannelMessage': - final String channel = call.arguments['channel']; - final String message = call.arguments['message']; + final String channel = call.arguments['channel']!; + final String message = call.arguments['message']!; _platformCallbacksHandler.onJavaScriptChannelMessage(channel, message); return true; case 'navigationRequest': return await _platformCallbacksHandler.onNavigationRequest( - url: call.arguments['url'], - isForMainFrame: call.arguments['isForMainFrame'], + url: call.arguments['url']!, + isForMainFrame: call.arguments['isForMainFrame']!, ); case 'onPageFinished': - _platformCallbacksHandler.onPageFinished(call.arguments['url']); + _platformCallbacksHandler.onPageFinished(call.arguments['url']!); return null; case 'onPageStarted': - _platformCallbacksHandler.onPageStarted(call.arguments['url']); + _platformCallbacksHandler.onPageStarted(call.arguments['url']!); return null; - case 'onBasicAuthRequest': - return await _platformCallbacksHandler - .onBasicAuthRequest(call.arguments['url']); case 'onWebResourceError': _platformCallbacksHandler.onWebResourceError( WebResourceError( - errorCode: call.arguments['errorCode'], - description: call.arguments['description'], - domain: call.arguments['domain'], + errorCode: call.arguments['errorCode']!, + description: call.arguments['description']!, + // iOS doesn't support `failingUrl`. failingUrl: call.arguments['failingUrl'], + domain: call.arguments['domain'], errorType: call.arguments['errorType'] == null ? null : WebResourceErrorType.values.firstWhere( @@ -74,7 +72,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { @override Future loadUrl( String url, - Map headers, + Map? headers, ) async { assert(url != null); return _channel.invokeMethod('loadUrl', { @@ -84,13 +82,15 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { } @override - Future currentUrl() => _channel.invokeMethod('currentUrl'); + Future currentUrl() => _channel.invokeMethod('currentUrl'); @override - Future canGoBack() => _channel.invokeMethod("canGoBack"); + Future canGoBack() => + _channel.invokeMethod("canGoBack").then((result) => result!); @override - Future canGoForward() => _channel.invokeMethod("canGoForward"); + Future canGoForward() => + _channel.invokeMethod("canGoForward").then((result) => result!); @override Future goBack() => _channel.invokeMethod("goBack"); @@ -105,18 +105,18 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { Future clearCache() => _channel.invokeMethod("clearCache"); @override - Future updateSettings(WebSettings settings) { + Future updateSettings(WebSettings settings) async { final Map updatesMap = _webSettingsToMap(settings); - if (updatesMap.isEmpty) { - return null; + if (updatesMap.isNotEmpty) { + await _channel.invokeMethod('updateSettings', updatesMap); } - return _channel.invokeMethod('updateSettings', updatesMap); } @override Future evaluateJavascript(String javascriptString) { - return _channel.invokeMethod( - 'evaluateJavascript', javascriptString); + return _channel + .invokeMethod('evaluateJavascript', javascriptString) + .then((result) => result!); } @override @@ -132,7 +132,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { } @override - Future getTitle() => _channel.invokeMethod("getTitle"); + Future getTitle() => _channel.invokeMethod("getTitle"); @override Future scrollTo(int x, int y) { @@ -151,19 +151,21 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { } @override - Future getScrollX() => _channel.invokeMethod("getScrollX"); + Future getScrollX() => + _channel.invokeMethod("getScrollX").then((result) => result!); @override - Future getScrollY() => _channel.invokeMethod("getScrollY"); + Future getScrollY() => + _channel.invokeMethod("getScrollY").then((result) => result!); /// Method channel implementation for [WebViewPlatform.clearCookies]. static Future clearCookies() { return _cookieManagerChannel .invokeMethod('clearCookies') - .then((dynamic result) => result); + .then((dynamic result) => result!); } - static Map _webSettingsToMap(WebSettings settings) { + static Map _webSettingsToMap(WebSettings? settings) { final Map map = {}; void _addIfNonNull(String key, dynamic value) { if (value == null) { @@ -179,11 +181,13 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { map[key] = setting.value; } - _addIfNonNull('jsMode', settings.javascriptMode?.index); + _addIfNonNull('jsMode', settings!.javascriptMode?.index); _addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); _addIfNonNull('debuggingEnabled', settings.debuggingEnabled); _addIfNonNull( 'gestureNavigationEnabled', settings.gestureNavigationEnabled); + _addIfNonNull( + 'allowsInlineMediaPlayback', settings.allowsInlineMediaPlayback); _addSettingIfPresent('userAgent', settings.userAgent); return map; } diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart index c3eb9df8f98f..4adad261ee79 100644 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ b/packages/webview_flutter/lib/webview_flutter.dart @@ -44,7 +44,7 @@ typedef void JavascriptMessageHandler(JavascriptMessage message); /// Information about a navigation action that is about to be executed. class NavigationRequest { - NavigationRequest._({this.url, this.isForMainFrame}); + NavigationRequest._({required this.url, required this.isForMainFrame}); /// The URL that will be loaded if the navigation is executed. final String url; @@ -79,11 +79,11 @@ enum NavigationDecision { class SurfaceAndroidWebView extends AndroidWebView { @override Widget build({ - BuildContext context, - CreationParams creationParams, - WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required BuildContext context, + required CreationParams creationParams, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, }) { assert(webViewPlatformCallbacksHandler != null); return PlatformViewLink( @@ -93,7 +93,7 @@ class SurfaceAndroidWebView extends AndroidWebView { PlatformViewController controller, ) { return AndroidViewSurface( - controller: controller, + controller: controller as AndroidViewController, gestureRecognizers: gestureRecognizers ?? const >{}, hitTestBehavior: PlatformViewHitTestBehavior.opaque, @@ -175,9 +175,9 @@ class JavascriptChannel { /// /// The parameters `name` and `onMessageReceived` must not be null. JavascriptChannel({ - @required this.name, - @required this.onMessageReceived, - }) : assert(name != null), + required this.name, + required this.onMessageReceived, + }) : assert(name != null), assert(onMessageReceived != null), assert(_validChannelNames.hasMatch(name)); @@ -211,7 +211,7 @@ class WebView extends StatefulWidget { /// /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. const WebView({ - Key key, + Key? key, this.onWebViewCreated, this.initialUrl, this.javascriptMode = JavascriptMode.disabled, @@ -227,11 +227,13 @@ class WebView extends StatefulWidget { this.userAgent, this.initialMediaPlaybackPolicy = AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + this.allowsInlineMediaPlayback = false, }) : assert(javascriptMode != null), assert(initialMediaPlaybackPolicy != null), + assert(allowsInlineMediaPlayback != null), super(key: key); - static WebViewPlatform _platform; + static WebViewPlatform? _platform; /// Sets a custom [WebViewPlatform]. /// @@ -240,7 +242,7 @@ class WebView extends StatefulWidget { /// Setting `platform` doesn't affect [WebView]s that were already created. /// /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. - static set platform(WebViewPlatform platform) { + static set platform(WebViewPlatform? platform) { _platform = platform; } @@ -261,11 +263,11 @@ class WebView extends StatefulWidget { "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); } } - return _platform; + return _platform!; } /// If not null invoked once the web view is created. - final WebViewCreatedCallback onWebViewCreated; + final WebViewCreatedCallback? onWebViewCreated; /// Which gestures should be consumed by the web view. /// @@ -276,10 +278,10 @@ class WebView extends StatefulWidget { /// /// When this set is empty or null, the web view will only handle pointer events for gestures that /// were not claimed by any other gesture recognizer. - final Set> gestureRecognizers; + final Set>? gestureRecognizers; /// The initial URL to load. - final String initialUrl; + final String? initialUrl; /// Whether Javascript execution is enabled. final JavascriptMode javascriptMode; @@ -311,7 +313,7 @@ class WebView extends StatefulWidget { /// channels in the list. /// /// A null value is equivalent to an empty set. - final Set javascriptChannels; + final Set? javascriptChannels; /// A delegate function that decides how to handle navigation actions. /// @@ -335,13 +337,20 @@ class WebView extends StatefulWidget { /// * When a navigationDelegate is set pages with frames are not properly handled by the /// webview, and frames will be opened in the main frame. /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. - final NavigationDelegate navigationDelegate; + final NavigationDelegate? navigationDelegate; + + /// Controls whether inline playback of HTML5 videos is allowed on iOS. + /// + /// This field is ignored on Android because Android allows it by default. + /// + /// By default `allowsInlineMediaPlayback` is false. + final bool allowsInlineMediaPlayback; /// Invoken when request asks for basic authentication final BasicAuthCallback onBasicAuthRequest; /// Invoked when a page starts loading. - final PageStartedCallback onPageStarted; + final PageStartedCallback? onPageStarted; /// Invoked when a page has finished loading. /// @@ -353,13 +362,13 @@ class WebView extends StatefulWidget { /// When invoked on iOS or Android, any Javascript code that is embedded /// directly in the HTML has been loaded and code injected with /// [WebViewController.evaluateJavascript] can assume this. - final PageFinishedCallback onPageFinished; + final PageFinishedCallback? onPageFinished; /// Invoked when a web resource has failed to load. /// /// This can be called for any resource (iframe, image, etc.), not just for /// the main page. - final WebResourceErrorCallback onWebResourceError; + final WebResourceErrorCallback? onWebResourceError; /// Controls whether WebView debugging is enabled. /// @@ -393,7 +402,7 @@ class WebView extends StatefulWidget { /// user agent. /// /// By default `userAgent` is null. - final String userAgent; + final String? userAgent; /// Which restrictions apply on automatic media playback. /// @@ -411,7 +420,7 @@ class _WebViewState extends State { final Completer _controller = Completer(); - _PlatformCallbacksHandler _platformCallbacksHandler; + late _PlatformCallbacksHandler _platformCallbacksHandler; @override Widget build(BuildContext context) { @@ -441,22 +450,22 @@ class _WebViewState extends State { }); } - void _onWebViewPlatformCreated(WebViewPlatformController webViewPlatform) { - final WebViewController controller = - WebViewController._(widget, webViewPlatform, _platformCallbacksHandler); + void _onWebViewPlatformCreated(WebViewPlatformController? webViewPlatform) { + final WebViewController controller = WebViewController._( + widget, webViewPlatform!, _platformCallbacksHandler); _controller.complete(controller); if (widget.onWebViewCreated != null) { - widget.onWebViewCreated(controller); + widget.onWebViewCreated!(controller); } } void _assertJavascriptChannelNamesAreUnique() { if (widget.javascriptChannels == null || - widget.javascriptChannels.isEmpty) { + widget.javascriptChannels!.isEmpty) { return; } assert(_extractChannelNames(widget.javascriptChannels).length == - widget.javascriptChannels.length); + widget.javascriptChannels!.length); } } @@ -476,7 +485,8 @@ WebSettings _webSettingsFromWidget(WebView widget) { hasNavigationDelegate: widget.navigationDelegate != null, debuggingEnabled: widget.debuggingEnabled, gestureNavigationEnabled: widget.gestureNavigationEnabled, - userAgent: WebSetting.of(widget.userAgent), + allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, + userAgent: WebSetting.of(widget.userAgent), ); } @@ -486,16 +496,16 @@ WebSettings _clearUnchangedWebSettings( assert(currentValue.javascriptMode != null); assert(currentValue.hasNavigationDelegate != null); assert(currentValue.debuggingEnabled != null); - assert(currentValue.userAgent.isPresent); + assert(currentValue.userAgent != null); assert(newValue.javascriptMode != null); assert(newValue.hasNavigationDelegate != null); assert(newValue.debuggingEnabled != null); - assert(newValue.userAgent.isPresent); + assert(newValue.userAgent != null); - JavascriptMode javascriptMode; - bool hasNavigationDelegate; - bool debuggingEnabled; - WebSetting userAgent = WebSetting.absent(); + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? debuggingEnabled; + WebSetting userAgent = WebSetting.absent(); if (currentValue.javascriptMode != newValue.javascriptMode) { javascriptMode = newValue.javascriptMode; } @@ -517,7 +527,7 @@ WebSettings _clearUnchangedWebSettings( ); } -Set _extractChannelNames(Set channels) { +Set _extractChannelNames(Set? channels) { final Set channelNames = channels == null ? {} : channels.map((JavascriptChannel channel) => channel.name).toSet(); @@ -537,15 +547,18 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { @override void onJavaScriptChannelMessage(String channel, String message) { - _javascriptChannels[channel].onMessageReceived(JavascriptMessage(message)); + _javascriptChannels[channel]!.onMessageReceived(JavascriptMessage(message)); } @override - FutureOr onNavigationRequest({String url, bool isForMainFrame}) async { + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) async { final NavigationRequest request = NavigationRequest._(url: url, isForMainFrame: isForMainFrame); final bool allowNavigation = _widget.navigationDelegate == null || - await _widget.navigationDelegate(request) == + await _widget.navigationDelegate!(request) == NavigationDecision.navigate; return allowNavigation; } @@ -553,25 +566,25 @@ class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { @override void onPageStarted(String url) { if (_widget.onPageStarted != null) { - _widget.onPageStarted(url); + _widget.onPageStarted!(url); } } @override void onPageFinished(String url) { if (_widget.onPageFinished != null) { - _widget.onPageFinished(url); + _widget.onPageFinished!(url); } } @override void onWebResourceError(WebResourceError error) { if (_widget.onWebResourceError != null) { - _widget.onWebResourceError(error); + _widget.onWebResourceError!(error); } } - void _updateJavascriptChannelsFromSet(Set channels) { + void _updateJavascriptChannelsFromSet(Set? channels) { _javascriptChannels.clear(); if (channels == null) { return; @@ -607,7 +620,7 @@ class WebViewController { final _PlatformCallbacksHandler _platformCallbacksHandler; - WebSettings _settings; + late WebSettings _settings; WebView _widget; @@ -621,7 +634,7 @@ class WebViewController { /// Throws an ArgumentError if `url` is not a valid URL string. Future loadUrl( String url, { - Map headers, + Map? headers, }) async { assert(url != null); _validateUrlString(url); @@ -635,7 +648,7 @@ class WebViewController { /// current URL changes again by the time this function returns (in other /// words, by the time this future completes, the WebView may be displaying a /// different URL). - Future currentUrl() { + Future currentUrl() { return _webViewPlatformController.currentUrl(); } @@ -703,7 +716,7 @@ class WebViewController { } Future _updateJavascriptChannels( - Set newChannels) async { + Set? newChannels) async { final Set currentChannels = _platformCallbacksHandler._javascriptChannels.keys.toSet(); final Set newChannelNames = _extractChannelNames(newChannels); @@ -742,10 +755,6 @@ class WebViewController { return Future.error(FlutterError( 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); } - if (javascriptString == null) { - return Future.error( - ArgumentError('The argument javascriptString must not be null.')); - } // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. // https://github.com/flutter/flutter/issues/26431 // ignore: strong_mode_implicit_dynamic_method @@ -753,7 +762,7 @@ class WebViewController { } /// Returns the title of the currently loaded page. - Future getTitle() { + Future getTitle() { return _webViewPlatformController.getTitle(); } @@ -795,7 +804,7 @@ class CookieManager { CookieManager._(); - static CookieManager _instance; + static CookieManager? _instance; /// Clears all cookies for all [WebView] instances. /// diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml index e4627a4f9f65..8bdd790e5e36 100644 --- a/packages/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/pubspec.yaml @@ -1,11 +1,11 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. -version: 1.0.7 +version: 2.0.0-nullsafety.3 homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.22.0 <2.0.0" + sdk: ">=2.12.0-0 <3.0.0" + flutter: ">=1.22.0" dependencies: flutter: @@ -16,7 +16,7 @@ dev_dependencies: sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0-nullsafety.1 flutter: plugin: diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index c7cf46a080d7..162b1932e49d 100644 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ b/packages/webview_flutter/test/webview_flutter_test.dart @@ -40,7 +40,7 @@ void main() { }); testWidgets('Initial url', (WidgetTester tester) async { - WebViewController controller; + late WebViewController controller; await tester.pumpWidget( WebView( initialUrl: 'https://youtube.com', @@ -60,7 +60,7 @@ void main() { )); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.javascriptMode, JavascriptMode.unrestricted); @@ -72,7 +72,7 @@ void main() { }); testWidgets('Load url', (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( onWebViewCreated: (WebViewController webViewController) { @@ -83,13 +83,13 @@ void main() { expect(controller, isNotNull); - await controller.loadUrl('https://flutter.io'); + await controller!.loadUrl('https://flutter.io'); - expect(await controller.currentUrl(), 'https://flutter.io'); + expect(await controller!.currentUrl(), 'https://flutter.io'); }); testWidgets('Invalid urls', (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( onWebViewCreated: (WebViewController webViewController) { @@ -100,19 +100,18 @@ void main() { expect(controller, isNotNull); - expect(() => controller.loadUrl(null), throwsA(anything)); - expect(await controller.currentUrl(), isNull); + expect(await controller!.currentUrl(), isNull); - expect(() => controller.loadUrl(''), throwsA(anything)); - expect(await controller.currentUrl(), isNull); + expect(() => controller!.loadUrl(''), throwsA(anything)); + expect(await controller!.currentUrl(), isNull); // Missing schema. - expect(() => controller.loadUrl('flutter.io'), throwsA(anything)); - expect(await controller.currentUrl(), isNull); + expect(() => controller!.loadUrl('flutter.io'), throwsA(anything)); + expect(await controller!.currentUrl(), isNull); }); testWidgets('Headers in loadUrl', (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( onWebViewCreated: (WebViewController webViewController) { @@ -126,13 +125,13 @@ void main() { final Map headers = { 'CACHE-CONTROL': 'ABC' }; - await controller.loadUrl('https://flutter.io', headers: headers); - expect(await controller.currentUrl(), equals('https://flutter.io')); + await controller!.loadUrl('https://flutter.io', headers: headers); + expect(await controller!.currentUrl(), equals('https://flutter.io')); }); testWidgets("Can't go back before loading a page", (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( onWebViewCreated: (WebViewController webViewController) { @@ -143,13 +142,13 @@ void main() { expect(controller, isNotNull); - final bool canGoBackNoPageLoaded = await controller.canGoBack(); + final bool canGoBackNoPageLoaded = await controller!.canGoBack(); expect(canGoBackNoPageLoaded, false); }); testWidgets("Clear Cache", (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( onWebViewCreated: (WebViewController webViewController) { @@ -159,15 +158,15 @@ void main() { ); expect(controller, isNotNull); - expect(fakePlatformViewsController.lastCreatedView.hasCache, true); + expect(fakePlatformViewsController.lastCreatedView!.hasCache, true); - await controller.clearCache(); + await controller!.clearCache(); - expect(fakePlatformViewsController.lastCreatedView.hasCache, false); + expect(fakePlatformViewsController.lastCreatedView!.hasCache, false); }); testWidgets("Can't go back with no history", (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( initialUrl: 'https://flutter.io', @@ -178,13 +177,13 @@ void main() { ); expect(controller, isNotNull); - final bool canGoBackFirstPageLoaded = await controller.canGoBack(); + final bool canGoBackFirstPageLoaded = await controller!.canGoBack(); expect(canGoBackFirstPageLoaded, false); }); testWidgets('Can go back', (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( initialUrl: 'https://flutter.io', @@ -196,15 +195,15 @@ void main() { expect(controller, isNotNull); - await controller.loadUrl('https://www.google.com'); - final bool canGoBackSecondPageLoaded = await controller.canGoBack(); + await controller!.loadUrl('https://www.google.com'); + final bool canGoBackSecondPageLoaded = await controller!.canGoBack(); expect(canGoBackSecondPageLoaded, true); }); testWidgets("Can't go forward before loading a page", (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( onWebViewCreated: (WebViewController webViewController) { @@ -215,13 +214,13 @@ void main() { expect(controller, isNotNull); - final bool canGoForwardNoPageLoaded = await controller.canGoForward(); + final bool canGoForwardNoPageLoaded = await controller!.canGoForward(); expect(canGoForwardNoPageLoaded, false); }); testWidgets("Can't go forward with no history", (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( initialUrl: 'https://flutter.io', @@ -232,13 +231,13 @@ void main() { ); expect(controller, isNotNull); - final bool canGoForwardFirstPageLoaded = await controller.canGoForward(); + final bool canGoForwardFirstPageLoaded = await controller!.canGoForward(); expect(canGoForwardFirstPageLoaded, false); }); testWidgets('Can go forward', (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( initialUrl: 'https://flutter.io', @@ -250,15 +249,15 @@ void main() { expect(controller, isNotNull); - await controller.loadUrl('https://youtube.com'); - await controller.goBack(); - final bool canGoForwardFirstPageBacked = await controller.canGoForward(); + await controller!.loadUrl('https://youtube.com'); + await controller!.goBack(); + final bool canGoForwardFirstPageBacked = await controller!.canGoForward(); expect(canGoForwardFirstPageBacked, true); }); testWidgets('Go back', (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( initialUrl: 'https://youtube.com', @@ -270,19 +269,19 @@ void main() { expect(controller, isNotNull); - expect(await controller.currentUrl(), 'https://youtube.com'); + expect(await controller!.currentUrl(), 'https://youtube.com'); - await controller.loadUrl('https://flutter.io'); + await controller!.loadUrl('https://flutter.io'); - expect(await controller.currentUrl(), 'https://flutter.io'); + expect(await controller!.currentUrl(), 'https://flutter.io'); - await controller.goBack(); + await controller!.goBack(); - expect(await controller.currentUrl(), 'https://youtube.com'); + expect(await controller!.currentUrl(), 'https://youtube.com'); }); testWidgets('Go forward', (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( initialUrl: 'https://youtube.com', @@ -294,23 +293,23 @@ void main() { expect(controller, isNotNull); - expect(await controller.currentUrl(), 'https://youtube.com'); + expect(await controller!.currentUrl(), 'https://youtube.com'); - await controller.loadUrl('https://flutter.io'); + await controller!.loadUrl('https://flutter.io'); - expect(await controller.currentUrl(), 'https://flutter.io'); + expect(await controller!.currentUrl(), 'https://flutter.io'); - await controller.goBack(); + await controller!.goBack(); - expect(await controller.currentUrl(), 'https://youtube.com'); + expect(await controller!.currentUrl(), 'https://youtube.com'); - await controller.goForward(); + await controller!.goForward(); - expect(await controller.currentUrl(), 'https://flutter.io'); + expect(await controller!.currentUrl(), 'https://flutter.io'); }); testWidgets('Current URL', (WidgetTester tester) async { - WebViewController controller; + WebViewController? controller; await tester.pumpWidget( WebView( onWebViewCreated: (WebViewController webViewController) { @@ -322,20 +321,20 @@ void main() { expect(controller, isNotNull); // Test a WebView without an explicitly set first URL. - expect(await controller.currentUrl(), isNull); + expect(await controller!.currentUrl(), isNull); - await controller.loadUrl('https://youtube.com'); - expect(await controller.currentUrl(), 'https://youtube.com'); + await controller!.loadUrl('https://youtube.com'); + expect(await controller!.currentUrl(), 'https://youtube.com'); - await controller.loadUrl('https://flutter.io'); - expect(await controller.currentUrl(), 'https://flutter.io'); + await controller!.loadUrl('https://flutter.io'); + expect(await controller!.currentUrl(), 'https://flutter.io'); - await controller.goBack(); - expect(await controller.currentUrl(), 'https://youtube.com'); + await controller!.goBack(); + expect(await controller!.currentUrl(), 'https://youtube.com'); }); testWidgets('Reload url', (WidgetTester tester) async { - WebViewController controller; + late WebViewController controller; await tester.pumpWidget( WebView( initialUrl: 'https://flutter.io', @@ -346,7 +345,7 @@ void main() { ); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.currentUrl, 'https://flutter.io'); expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); @@ -362,7 +361,7 @@ void main() { }); testWidgets('evaluate Javascript', (WidgetTester tester) async { - WebViewController controller; + late WebViewController controller; await tester.pumpWidget( WebView( initialUrl: 'https://flutter.io', @@ -375,15 +374,11 @@ void main() { expect( await controller.evaluateJavascript("fake js string"), "fake js string", reason: 'should get the argument'); - expect( - () => controller.evaluateJavascript(null), - throwsA(anything), - ); }); testWidgets('evaluate Javascript with JavascriptMode disabled', (WidgetTester tester) async { - WebViewController controller; + late WebViewController controller; await tester.pumpWidget( WebView( initialUrl: 'https://flutter.io', @@ -397,10 +392,6 @@ void main() { () => controller.evaluateJavascript('fake js string'), throwsA(anything), ); - expect( - () => controller.evaluateJavascript(null), - throwsA(anything), - ); }); testWidgets('Cookies can be cleared once', (WidgetTester tester) async { @@ -432,19 +423,17 @@ void main() { await tester.pumpWidget( WebView( initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ + javascriptChannels: { JavascriptChannel( name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), JavascriptChannel( name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - ].toSet(), + }, ), ); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.javascriptChannelNames, unorderedEquals(['Tts', 'Alarm'])); @@ -472,14 +461,12 @@ void main() { await tester.pumpWidget( WebView( initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ + javascriptChannels: { JavascriptChannel( name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), JavascriptChannel( name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - ].toSet(), + }, ), ); expect(tester.takeException(), isNot(null)); @@ -489,35 +476,31 @@ void main() { await tester.pumpWidget( WebView( initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ + javascriptChannels: { JavascriptChannel( name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), JavascriptChannel( name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - ].toSet(), + }, ), ); await tester.pumpWidget( WebView( initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ + javascriptChannels: { JavascriptChannel( name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), JavascriptChannel( name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}), JavascriptChannel( name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}), - ].toSet(), + }, ), ); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.javascriptChannelNames, unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); @@ -532,12 +515,10 @@ void main() { await tester.pumpWidget( WebView( initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ + javascriptChannels: { JavascriptChannel( name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - ].toSet(), + }, ), ); @@ -550,17 +531,15 @@ void main() { await tester.pumpWidget( WebView( initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ + javascriptChannels: { JavascriptChannel( name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - ].toSet(), + }, ), ); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.javascriptChannelNames, unorderedEquals(['Tts'])); @@ -572,9 +551,7 @@ void main() { await tester.pumpWidget( WebView( initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ + javascriptChannels: { JavascriptChannel( name: 'Tts', onMessageReceived: (JavascriptMessage msg) { @@ -585,12 +562,12 @@ void main() { onMessageReceived: (JavascriptMessage msg) { alarmMessagesReceived.add(msg.message); }), - ].toSet(), + }, ), ); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(ttsMessagesReceived, isEmpty); expect(alarmMessagesReceived, isEmpty); @@ -603,7 +580,7 @@ void main() { group('$PageStartedCallback', () { testWidgets('onPageStarted is not null', (WidgetTester tester) async { - String returnedUrl; + String? returnedUrl; await tester.pumpWidget(WebView( initialUrl: 'https://youtube.com', @@ -613,7 +590,7 @@ void main() { )); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; platformWebView.fakeOnPageStartedCallback(); @@ -627,7 +604,7 @@ void main() { )); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; // The platform side will always invoke a call for onPageStarted. This is // to test that it does not crash on a null callback. @@ -635,7 +612,7 @@ void main() { }); testWidgets('onPageStarted changed', (WidgetTester tester) async { - String returnedUrl; + String? returnedUrl; await tester.pumpWidget(WebView( initialUrl: 'https://youtube.com', @@ -650,7 +627,7 @@ void main() { )); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; platformWebView.fakeOnPageStartedCallback(); @@ -660,7 +637,7 @@ void main() { group('$PageFinishedCallback', () { testWidgets('onPageFinished is not null', (WidgetTester tester) async { - String returnedUrl; + String? returnedUrl; await tester.pumpWidget(WebView( initialUrl: 'https://youtube.com', @@ -670,7 +647,7 @@ void main() { )); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; platformWebView.fakeOnPageFinishedCallback(); @@ -684,7 +661,7 @@ void main() { )); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; // The platform side will always invoke a call for onPageFinished. This is // to test that it does not crash on a null callback. @@ -692,7 +669,7 @@ void main() { }); testWidgets('onPageFinished changed', (WidgetTester tester) async { - String returnedUrl; + String? returnedUrl; await tester.pumpWidget(WebView( initialUrl: 'https://youtube.com', @@ -707,7 +684,7 @@ void main() { )); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; platformWebView.fakeOnPageFinishedCallback(); @@ -722,13 +699,14 @@ void main() { )); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.hasNavigationDelegate, false); await tester.pumpWidget(WebView( initialUrl: 'https://youtube.com', - navigationDelegate: (NavigationRequest r) => null, + navigationDelegate: (NavigationRequest r) => + NavigationDecision.navigate, )); expect(platformWebView.hasNavigationDelegate, true); @@ -748,7 +726,7 @@ void main() { })); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.hasNavigationDelegate, true); @@ -773,7 +751,7 @@ void main() { )); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.debuggingEnabled, true); }); @@ -782,7 +760,7 @@ void main() { await tester.pumpWidget(const WebView()); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.debuggingEnabled, false); }); @@ -792,7 +770,7 @@ void main() { await tester.pumpWidget(WebView(key: key)); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; await tester.pumpWidget(WebView( key: key, @@ -826,8 +804,8 @@ void main() { ), ); - final MyWebViewPlatform builder = WebView.platform; - final MyWebViewPlatformController platform = builder.lastPlatformBuilt; + final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform; + final MyWebViewPlatformController platform = builder.lastPlatformBuilt!; expect( platform.creationParams, @@ -837,17 +815,14 @@ void main() { javascriptMode: JavascriptMode.disabled, hasNavigationDelegate: false, debuggingEnabled: false, - userAgent: WebSetting.of(null), + userAgent: WebSetting.of(null), gestureNavigationEnabled: true, ), - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannelNames: Set(), ))); }); testWidgets('loadUrl', (WidgetTester tester) async { - WebViewController controller; + late WebViewController controller; await tester.pumpWidget( WebView( initialUrl: 'https://youtube.com', @@ -857,8 +832,8 @@ void main() { ), ); - final MyWebViewPlatform builder = WebView.platform; - final MyWebViewPlatformController platform = builder.lastPlatformBuilt; + final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform; + final MyWebViewPlatformController platform = builder.lastPlatformBuilt!; final Map headers = { 'header': 'value', @@ -877,7 +852,7 @@ void main() { )); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.userAgent, isNull); @@ -892,9 +867,9 @@ void main() { } class FakePlatformWebView { - FakePlatformWebView(int id, Map params) { + FakePlatformWebView(int? id, Map params) { if (params.containsKey('initialUrl')) { - final String initialUrl = params['initialUrl']; + final String? initialUrl = params['initialUrl']; if (initialUrl != null) { history.add(initialUrl); currentPosition++; @@ -914,20 +889,20 @@ class FakePlatformWebView { channel.setMockMethodCallHandler(onMethodCall); } - MethodChannel channel; + late MethodChannel channel; - List history = []; + List history = []; int currentPosition = -1; int amountOfReloadsOnCurrentUrl = 0; bool hasCache = true; - String get currentUrl => history.isEmpty ? null : history[currentPosition]; - JavascriptMode javascriptMode; - List javascriptChannelNames; + String? get currentUrl => history.isEmpty ? null : history[currentPosition]; + JavascriptMode? javascriptMode; + List? javascriptChannelNames; - bool hasNavigationDelegate; - bool debuggingEnabled; - String userAgent; + bool? hasNavigationDelegate; + bool? debuggingEnabled; + String? userAgent; Future onMethodCall(MethodCall call) { switch (call.method) { @@ -949,34 +924,28 @@ class FakePlatformWebView { break; case 'canGoBack': return Future.sync(() => currentPosition > 0); - break; case 'canGoForward': return Future.sync(() => currentPosition < history.length - 1); - break; case 'goBack': currentPosition = max(-1, currentPosition - 1); return Future.sync(() {}); - break; case 'goForward': currentPosition = min(history.length - 1, currentPosition + 1); return Future.sync(() {}); case 'reload': amountOfReloadsOnCurrentUrl++; return Future.sync(() {}); - break; case 'currentUrl': - return Future.value(currentUrl); - break; + return Future.value(currentUrl); case 'evaluateJavascript': return Future.value(call.arguments); - break; case 'addJavascriptChannels': final List channelNames = List.from(call.arguments); - javascriptChannelNames.addAll(channelNames); + javascriptChannelNames!.addAll(channelNames); break; case 'removeJavascriptChannels': final List channelNames = List.from(call.arguments); - javascriptChannelNames + javascriptChannelNames! .removeWhere((String channel) => channelNames.contains(channel)); break; case 'clearCache': @@ -994,14 +963,14 @@ class FakePlatformWebView { }; final ByteData data = codec .encodeMethodCall(MethodCall('javascriptChannelMessage', arguments)); - ServicesBinding.instance.defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData data) {}); + ServicesBinding.instance!.defaultBinaryMessenger + .handlePlatformMessage(channel.name, data, (ByteData? data) {}); } // Fakes a main frame navigation that was initiated by the webview, e.g when // the user clicks a link in the currently loaded page. void fakeNavigate(String url) { - if (!hasNavigationDelegate) { + if (!hasNavigationDelegate!) { print('no navigation delegate'); _loadUrl(url); return; @@ -1013,9 +982,9 @@ class FakePlatformWebView { }; final ByteData data = codec.encodeMethodCall(MethodCall('navigationRequest', arguments)); - ServicesBinding.instance.defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData data) { - final bool allow = codec.decodeEnvelope(data); + ServicesBinding.instance!.defaultBinaryMessenger + .handlePlatformMessage(channel.name, data, (ByteData? data) { + final bool allow = codec.decodeEnvelope(data!); if (allow) { _loadUrl(url); } @@ -1030,10 +999,10 @@ class FakePlatformWebView { {'url': currentUrl}, )); - ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( channel.name, data, - (ByteData data) {}, + (ByteData? data) {}, ); } @@ -1045,14 +1014,14 @@ class FakePlatformWebView { {'url': currentUrl}, )); - ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( channel.name, data, - (ByteData data) {}, + (ByteData? data) {}, ); } - void _loadUrl(String url) { + void _loadUrl(String? url) { history = history.sublist(0, currentPosition + 1); history.add(url); currentPosition++; @@ -1061,13 +1030,13 @@ class FakePlatformWebView { } class _FakePlatformViewsController { - FakePlatformWebView lastCreatedView; + FakePlatformWebView? lastCreatedView; Future fakePlatformViewsMethodHandler(MethodCall call) { switch (call.method) { case 'create': final Map args = call.arguments; - final Map params = _decodeParams(args['params']); + final Map params = _decodeParams(args['params'])!; lastCreatedView = FakePlatformWebView( args['id'], params, @@ -1083,7 +1052,7 @@ class _FakePlatformViewsController { } } -Map _decodeParams(Uint8List paramsMessage) { +Map? _decodeParams(Uint8List paramsMessage) { final ByteBuffer buffer = paramsMessage.buffer; final ByteData messageBytes = buffer.asByteData( paramsMessage.offsetInBytes, @@ -1114,9 +1083,8 @@ class _FakeCookieManager { return Future.sync(() { return hadCookies; }); - break; } - return Future.sync(() => null); + return Future.sync(() => true); } void reset() { @@ -1125,26 +1093,26 @@ class _FakeCookieManager { } class MyWebViewPlatform implements WebViewPlatform { - MyWebViewPlatformController lastPlatformBuilt; + MyWebViewPlatformController? lastPlatformBuilt; @override Widget build({ - BuildContext context, - CreationParams creationParams, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - @required WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, + BuildContext? context, + CreationParams? creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, }) { assert(onWebViewPlatformCreated != null); lastPlatformBuilt = MyWebViewPlatformController( creationParams, gestureRecognizers, webViewPlatformCallbacksHandler); - onWebViewPlatformCreated(lastPlatformBuilt); + onWebViewPlatformCreated!(lastPlatformBuilt); return Container(); } @override Future clearCookies() { - return Future.sync(() => null); + return Future.sync(() => true); } } @@ -1153,25 +1121,24 @@ class MyWebViewPlatformController extends WebViewPlatformController { WebViewPlatformCallbacksHandler platformHandler) : super(platformHandler); - CreationParams creationParams; - Set> gestureRecognizers; + CreationParams? creationParams; + Set>? gestureRecognizers; - String lastUrlLoaded; - Map lastRequestHeaders; + String? lastUrlLoaded; + Map? lastRequestHeaders; @override - Future loadUrl(String url, Map headers) { + Future loadUrl(String url, Map? headers) async { equals(1, 1); lastUrlLoaded = url; lastRequestHeaders = headers; - return null; } } class MatchesWebSettings extends Matcher { MatchesWebSettings(this._webSettings); - final WebSettings _webSettings; + final WebSettings? _webSettings; @override Description describe(Description description) => @@ -1180,13 +1147,13 @@ class MatchesWebSettings extends Matcher { @override bool matches( covariant WebSettings webSettings, Map matchState) { - return _webSettings.javascriptMode == webSettings.javascriptMode && - _webSettings.hasNavigationDelegate == + return _webSettings!.javascriptMode == webSettings.javascriptMode && + _webSettings!.hasNavigationDelegate == webSettings.hasNavigationDelegate && - _webSettings.debuggingEnabled == webSettings.debuggingEnabled && - _webSettings.gestureNavigationEnabled == + _webSettings!.debuggingEnabled == webSettings.debuggingEnabled && + _webSettings!.gestureNavigationEnabled == webSettings.gestureNavigationEnabled && - _webSettings.userAgent == webSettings.userAgent; + _webSettings!.userAgent == webSettings.userAgent; } } @@ -1204,7 +1171,7 @@ class MatchesCreationParams extends Matcher { Map matchState) { return _creationParams.initialUrl == creationParams.initialUrl && MatchesWebSettings(_creationParams.webSettings) - .matches(creationParams.webSettings, matchState) && + .matches(creationParams.webSettings!, matchState) && orderedEquals(_creationParams.javascriptChannelNames) .matches(creationParams.javascriptChannelNames, matchState); } diff --git a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md index 37711dcac49c..5c3ea32e8ef9 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md +++ b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.0.3 + +* Fix README example. + +## 1.0.2 + +* Update Flutter SDK constraint. + ## 1.0.1 * Fixed method channel name in android implementation. [Issue](https://github.com/flutter/flutter/issues/69073). diff --git a/packages/wifi_info_flutter/wifi_info_flutter/README.md b/packages/wifi_info_flutter/wifi_info_flutter/README.md index 0c2eff383c16..e1c0c84dc04b 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/README.md +++ b/packages/wifi_info_flutter/wifi_info_flutter/README.md @@ -24,9 +24,9 @@ You can get wi-fi related information using: ```dart import 'package:wifi_info_flutter/wifi_info_flutter.dart'; -var wifiBSSID = await WifiFlutter().getWifiBSSID(); -var wifiIP = await WifiFlutter().getWifiIP(); -var wifiName = await WifiFlutter().getWifiName(); +var wifiBSSID = await WifiInfo().getWifiBSSID(); +var wifiIP = await WifiInfo().getWifiIP(); +var wifiName = await WifiInfo().getWifiName(); ``` ### iOS 12 diff --git a/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml index 74ee9f074cb5..4f5ecafb50b4 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml +++ b/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml @@ -1,11 +1,11 @@ name: wifi_info_flutter description: A new flutter plugin project. -version: 1.0.1 +version: 1.0.3 homepage: https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter/wifi_info_flutter environment: sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.20.0 <2.0.0" + flutter: ">=1.20.0" dependencies: flutter: diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md index a845f1956ab4..951f8f5f5ae0 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.1 + +* Update Flutter SDK constraint. + ## 1.0.0 * Initial release of package. Includes support for retrieving wifi name, wifi BSSID, wifi ip address diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml index 68d516d515a8..62bffd0eeeb3 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml @@ -1,13 +1,13 @@ name: wifi_info_flutter_platform_interface description: A common platform interface for the wifi_info_flutter plugin. -version: 1.0.0 +version: 1.0.1 # 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 homepage: https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter/wifi_info_flutter_platform_interface environment: sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.17.0 <2.0.0" + flutter: ">=1.17.0" dependencies: plugin_platform_interface: ^1.0.3 diff --git a/script/build_all_plugins_app.sh b/script/build_all_plugins_app.sh index f1f68ddbe985..ca97c05f8ee4 100755 --- a/script/build_all_plugins_app.sh +++ b/script/build_all_plugins_app.sh @@ -1,13 +1,21 @@ #!/bin/bash +# Usage: +# +# ./script/build_all_plugins_app.sh apk +# ./script/build_all_plugins_app.sh ios + # This script builds the app in flutter/plugins/example/all_plugins to make # sure all first party plugins can be compiled together. # So that users can run this script from anywhere and it will work as expected. readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null && pwd)" + readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" source "$SCRIPT_DIR/common.sh" +source "$SCRIPT_DIR/nnbd_plugins.sh" + check_changed_packages > /dev/null readonly EXCLUDED_PLUGINS_LIST=( @@ -21,6 +29,7 @@ readonly EXCLUDED_PLUGINS_LIST=( "google_sign_in_platform_interface" "google_sign_in_web" "image_picker_platform_interface" + "local_auth" # flutter_plugin_android_lifecycle conflict "instrumentation_adapter" "path_provider_linux" "path_provider_macos" @@ -42,7 +51,15 @@ readonly EXCLUDED_PLUGINS_LIST=( # Comma-separated string of the list above readonly EXCLUDED=$(IFS=, ; echo "${EXCLUDED_PLUGINS_LIST[*]}") -(cd "$REPO_DIR" && pub global run flutter_plugin_tools all-plugins-app --exclude $EXCLUDED) +ALL_EXCLUDED=($EXCLUDED) +# Exclude nnbd plugins from stable. +if [ "$CHANNEL" == "stable" ]; then + ALL_EXCLUDED=("$EXCLUDED,$EXCLUDED_PLUGINS_FROM_STABLE") +fi + +echo "Excluding the following plugins: $ALL_EXCLUDED" + +(cd "$REPO_DIR" && pub global run flutter_plugin_tools all-plugins-app --exclude $ALL_EXCLUDED) function error() { echo "$@" 1>&2 diff --git a/script/incremental_build.sh b/script/incremental_build.sh index f89bc1d0e5c9..3911f0a6e9c8 100755 --- a/script/incremental_build.sh +++ b/script/incremental_build.sh @@ -5,6 +5,7 @@ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" source "$SCRIPT_DIR/common.sh" +source "$SCRIPT_DIR/nnbd_plugins.sh" if [ "$(expr substr $(uname -s) 1 5)" == "MINGW" ]; then PUB=pub.bat @@ -12,6 +13,14 @@ else PUB=pub fi +# Plugins that are excluded from this task. +ALL_EXCLUDED=("") +# Exclude nnbd plugins from stable. +if [ "$CHANNEL" == "stable" ]; then + ALL_EXCLUDED=($EXCLUDED_PLUGINS_FROM_STABLE) + echo "Excluding the following plugins: $ALL_EXCLUDED" +fi + # Plugins that deliberately use their own analysis_options.yaml. # # This list should only be deleted from, never added to. This only exists @@ -21,6 +30,7 @@ fi # TODO(mklim): Remove everything from this list. https://github.com/flutter/flutter/issues/45440 CUSTOM_ANALYSIS_PLUGINS=( ) + # Comma-separated string of the list above readonly CUSTOM_FLAG=$(IFS=, ; echo "${CUSTOM_ANALYSIS_PLUGINS[*]}") # Set some default actions if run without arguments. @@ -39,7 +49,7 @@ PLUGIN_SHARDING=($PLUGIN_SHARDING) if [[ "${BRANCH_NAME}" == "master" ]]; then echo "Running for all packages" - (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" ${PLUGIN_SHARDING[@]}) + (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) else # Sets CHANGED_PACKAGES check_changed_packages @@ -47,11 +57,12 @@ else if [[ "$CHANGED_PACKAGES" == "" ]]; then echo "No changes detected in packages." echo "Running for all packages" - (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" ${PLUGIN_SHARDING[@]}) + (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) else echo running "${ACTIONS[@]}" - (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --plugins="$CHANGED_PACKAGES" ${PLUGIN_SHARDING[@]}) + (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --plugins="$CHANGED_PACKAGES" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) echo "Running version check for changed packages" - (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools version-check --base_sha="$(get_branch_base_sha)") + # TODO(egarciad): Enable this check once in master. + # (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools version-check --base_sha="$(get_branch_base_sha)") fi fi diff --git a/script/nnbd_plugins.sh b/script/nnbd_plugins.sh new file mode 100644 index 000000000000..7bc5ac35a3a5 --- /dev/null +++ b/script/nnbd_plugins.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# This script contains the list of plugins migrated to nnbd +# that should be excluded from testing on Flutter stable until +# null-safe is available on stable. + +readonly NNBD_PLUGINS_LIST=( + "android_intent" + "battery" + "connectivity" + "device_info" + "flutter_plugin_android_lifecycle" + "flutter_webview" + "google_sign_in" + "local_auth" + "path_provider" + "plugin_platform_interface" + "share" + "url_launcher" + "video_player" + "webview_flutter" +) + +export EXCLUDED_PLUGINS_FROM_STABLE=$(IFS=, ; echo "${NNBD_PLUGINS_LIST[*]}")