diff --git a/.cirrus.yml b/.cirrus.yml index 98cd6276e0e6..5a25b773ea33 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -11,10 +11,14 @@ task: upgrade_script: - flutter channel stable - flutter upgrade + - flutter channel beta + - flutter upgrade - flutter channel master - flutter upgrade - git fetch origin master - activate_script: pub global activate flutter_plugin_tools + submodules_script: + - git submodule init + - git submodule update matrix: - name: publishable script: @@ -31,6 +35,7 @@ task: env: matrix: CHANNEL: "master" + CHANNEL: "beta" CHANNEL: "stable" test_script: # TODO(jackson): Allow web plugins once supported on stable @@ -39,8 +44,18 @@ task: - flutter channel $CHANNEL - ./script/incremental_build.sh test - name: analyze + env: + matrix: + CHANNEL: "master" + CHANNEL: "beta" + CHANNEL: "stable" script: ./script/incremental_build.sh analyze - name: build_all_plugins_apk + env: + matrix: + CHANNEL: "master" + CHANNEL: "beta" + CHANNEL: "stable" script: # TODO(jackson): Allow web plugins once supported on stable # https://github.com/flutter/flutter/issues/42864 @@ -69,6 +84,7 @@ task: PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" matrix: CHANNEL: "master" + CHANNEL: "beta" CHANNEL: "stable" MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[07586610af1fdfc894e5969f70ef2458341b9b7e9c3b7c4225a663b4a48732b7208a4d91c3b7d45305a6b55fa2a37fc4] @@ -89,11 +105,11 @@ task: - export CIRRUS_COMMIT_MESSAGE="" - ./script/incremental_build.sh build-examples --apk - ./script/incremental_build.sh java-test # must come after apk build - - if [[ $GCLOUD_FIREBASE_TESTLAB_KEY == ENCRYPTED* ]]; then - - echo "This user does not have permission to run Firebase Test Lab tests." - - else + - if [[ -n "$GCLOUD_FIREBASE_TESTLAB_KEY" ]]; then - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - ./script/incremental_build.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 + - else + - echo "This user does not have permission to run Firebase Test Lab tests." - fi - export CIRRUS_CHANGE_MESSAGE=`cat /tmp/cirrus_change_message.txt` - export CIRRUS_COMMIT_MESSAGE=`cat /tmp/cirrus_commit_message.txt` @@ -111,10 +127,14 @@ task: upgrade_script: - flutter channel stable - flutter upgrade + - flutter channel beta + - flutter upgrade - flutter channel master - flutter upgrade - git fetch origin master - activate_script: pub global activate flutter_plugin_tools + submodules_script: + - git submodule init + - git submodule update matrix: - name: build-linux+drive-examples install_script: @@ -126,44 +146,44 @@ task: - xvfb-run ./script/incremental_build.sh drive-examples --linux task: + # Xcode 12 task # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins only_if: $CIRRUS_TAG == '' use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' osx_instance: - image: catalina-xcode-11.3.1-flutter + image: big-sur-xcode-12.3 upgrade_script: + - sudo gem install cocoapods - flutter channel stable - flutter upgrade + - flutter channel beta + - flutter upgrade - flutter channel master - flutter upgrade - git fetch origin master - activate_script: pub global activate flutter_plugin_tools + submodules_script: + - git submodule init + - git submodule update create_simulator_script: - xcrun simctl list - - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot + - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-11 com.apple.CoreSimulator.SimRuntime.iOS-14-3 | xargs xcrun simctl boot matrix: - name: build_all_plugins_ipa + env: + matrix: + CHANNEL: "master" + CHANNEL: "beta" + CHANNEL: "stable" script: # TODO(jackson): Allow web plugins once supported on stable # https://github.com/flutter/flutter/issues/42864 - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - flutter channel $CHANNEL - ./script/build_all_plugins_app.sh ios --no-codesign - - name: lint_darwin_plugins - env: - matrix: - PLUGIN_SHARDING: "--shardIndex 0 --shardCount 2" - PLUGIN_SHARDING: "--shardIndex 1 --shardCount 2" - script: - # TODO(jmagman): Lint macOS podspecs but skip any that fail library validation. - - find . -name "*.podspec" | xargs grep -l "osx" | xargs rm - # Skip the dummy podspecs used to placate the tool. - - find . -name "*_web*.podspec" -o -name "*_mac*.podspec" | xargs rm - - ./script/incremental_build.sh podspecs - name: build-ipas+drive-examples env: PATH: $PATH:/usr/local/bin - PLUGINS_TO_SKIP_XCTESTS: "battery/battery,camera/camera,connectivity/connectivity,device_info/device_info,espresso,google_maps_flutter/google_maps_flutter,google_sign_in/google_sign_in,in_app_purchase,integration_test,ios_platform_images,local_auth,package_info,path_provider/path_provider,quick_actions,sensors,shared_preferences/shared_preferences,url_launcher/url_launcher,video_player/video_player,webview_flutter,wifi_info_flutter/wifi_info_flutter" + PLUGINS_TO_SKIP_XCTESTS: "battery/battery,camera/camera,connectivity/connectivity,device_info/device_info,espresso,google_maps_flutter/google_maps_flutter,google_sign_in/google_sign_in,in_app_purchase,integration_test,ios_platform_images,local_auth,package_info,path_provider/path_provider,sensors,shared_preferences/shared_preferences,url_launcher/url_launcher,video_player/video_player,webview_flutter,wifi_info_flutter/wifi_info_flutter" matrix: PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" PLUGIN_SHARDING: "--shardIndex 1 --shardCount 4" @@ -171,6 +191,7 @@ task: PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" matrix: CHANNEL: "master" + CHANNEL: "beta" CHANNEL: "stable" SIMCTL_CHILD_MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] build_script: @@ -178,22 +199,64 @@ task: # https://github.com/flutter/flutter/issues/42864 - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - flutter channel $CHANNEL + - flutter upgrade - ./script/incremental_build.sh build-examples --ipa - - ./script/incremental_build.sh drive-examples - - ./script/incremental_build.sh xctest --target RunnerUITests --skip $PLUGINS_TO_SKIP_XCTESTS + - ./script/incremental_build.sh xctest --target RunnerUITests --skip $PLUGINS_TO_SKIP_XCTESTS --ios-destination "platform=iOS Simulator,name=iPhone 11,OS=14.3" + # `drive-examples` contains integration tests, which changes the UI of the application. + # This UI change sometimes affects `xctest`. + # So we run `drive-examples` after `xctest`, changing the order will result ci failure. + - ./script/incremental_build.sh drive-examples --ios + task: + # Xcode 11 task + # TODO(cyanglaz): merge Xcode 11 task to Xcode 12 task when all the matrix can be run in Xcode 12. # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins only_if: $CIRRUS_TAG == '' use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' osx_instance: image: catalina-xcode-11.3.1-flutter + upgrade_script: + - sudo gem install cocoapods + - flutter channel stable + - flutter upgrade + - flutter channel master + - flutter upgrade + - git fetch origin master + submodules_script: + - git submodule init + - git submodule update + create_simulator_script: + - xcrun simctl list + - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot + matrix: + - name: lint_darwin_plugins + env: + matrix: + PLUGIN_SHARDING: "--shardIndex 0 --shardCount 2" + PLUGIN_SHARDING: "--shardIndex 1 --shardCount 2" + script: + # TODO(jmagman): Lint macOS podspecs but skip any that fail library validation. + - find . -name "*.podspec" | xargs grep -l "osx" | xargs rm + # Skip the dummy podspecs used to placate the tool. + - find . -name "*_web*.podspec" -o -name "*_mac*.podspec" | xargs rm + - ./script/incremental_build.sh podspecs + +task: + # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins + only_if: $CIRRUS_TAG == '' + use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' + osx_instance: + image: big-sur-xcode-12.3 setup_script: - flutter config --enable-macos-desktop upgrade_script: + - sudo gem install cocoapods - flutter channel master - flutter upgrade - git fetch origin master - activate_script: pub global activate flutter_plugin_tools + submodules_script: + - git submodule init + - git submodule update matrix: - name: build_all_plugins_app script: 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/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000000..cdb9ade0e7f8 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,110 @@ +'p: android_alarm_manager': + - packages/android_alarm_manager/**/* + +'p: android_intent': + - packages/android_intent/**/* + +'p: battery': + - packages/battery/**/* + +'p: camera': + - packages/camera/**/* + +'p: connectivity': + - packages/connectivity/**/* + +'p: cross_file': + - packages/cross_file/**/* + +'p: device_info': + - packages/device_info/**/* + +'p: e2e': + - packages/e2e/**/* + +'p: espresso': + - packages/espresso/**/* + +'p: file_selector': + - packages/file_selector/**/* + +'p: flutter_plugin_android_lifecycle': + - packages/flutter_plugin_android_lifecycle/**/* + +'p: google_maps_flutter': + - packages/google_maps_flutter/**/* + +'p: google_sign_in': + - packages/google_sign_in/**/* + +'p: image_picker': + - packages/image_picker/**/* + +'p: in_app_purchase': + - packages/in_app_purchase/**/* + +'p: integration_test': + - packages/integration_test/**/* + +'p: ios_platform_images': + - packages/ios_platform_images/**/* + +'p: local_auth': + - packages/local_auth/**/* + +'p: package_info': + - packages/package_info/**/* + +'p: path_provider': + - packages/path_provider/**/* + +'p: plugin_platform_interface': + - packages/plugin_platform_interface/**/* + +'p: quick_actions': + - packages/quick_actions/**/* + +'p: sensors': + - packages/sensors/**/* + +'p: share': + - packages/share/**/* + +'p: shared_preferences': + - packages/shared_preferences/**/* + +'p: url_launcher': + - packages/url_launcher/**/* + +'p: video_player': + - packages/video_player/**/* + +'p: webview_flutter': + - packages/webview_flutter/**/* + +'p: wifi_info_flutter': + - packages/wifi_info_flutter/**/* + +'platform-android': + - packages/*/*_android/**/* + - packages/**/android/**/* + +'platform-ios': + - packages/*/*_ios/**/* + - packages/**/ios/**/* + +'platform-linux': + - packages/*/*_linux/**/* + - packages/**/linux/**/* + +'platform-macos': + - packages/*/*_macos/**/* + - packages/**/macos/**/* + +'platform-web': + - packages/*/*_web/**/* + - packages/**/web/**/* + +'platform-windows': + - packages/*/*_windows/**/* + - packages/**/windows/**/* diff --git a/.github/post_merge_labeler.yml b/.github/post_merge_labeler.yml new file mode 100644 index 000000000000..c02eb411773e --- /dev/null +++ b/.github/post_merge_labeler.yml @@ -0,0 +1,2 @@ +'needs-publishing': + - packages/*/** diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml new file mode 100644 index 000000000000..6b93864d3f3a --- /dev/null +++ b/.github/workflows/pull_request_label.yml @@ -0,0 +1,31 @@ +# This workflow applies labels to pull requests based on the +# paths that are modified in the pull request. +# +# Edit `.github/labeler.yml` and `.github/post_merge_labeler.yml` +# to configure labels. +# +# For more information, see: https://github.com/actions/labeler + +name: Pull Request Labeler + +on: + pull_request_target: + types: [opened, synchronize, reopened, closed] + +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@9794b1493b6f1fa7b006c5f8635a19c76c98be95 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + sync-labels: true + + post_merge_label: + if: github.event.action == 'closed' && github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@9794b1493b6f1fa7b006c5f8635a19c76c98be95 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/post_merge_labeler.yml diff --git a/.gitignore b/.gitignore index d7560505f166..3582a15fae57 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ build/ .project .classpath .settings + +# Downloaded by the plugin tools. +google-java-format-1.3-all-deps.jar diff --git a/AUTHORS b/AUTHORS index 51345c9a3481..dbf9d190931b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -59,3 +59,7 @@ Kazuki Yamaguchi Eitan Schwartz Chris Rutkowski Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/CODEOWNERS b/CODEOWNERS index 160c0d5a3d10..01732888ad89 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,23 +4,12 @@ # These names are just suggestions. It is fine to have your changes # reviewed by someone else. -packages/android_alarm_manager/** @bkonyi -packages/android_intent/** @mklim @matthew-carroll -packages/battery/** @amirh @matthew-carroll + packages/camera/** @bparrishMines -packages/connectivity/** @cyanglaz @matthew-carroll -packages/device_info/** @matthew-carroll -packages/espresso/** @collinjackson @adazh +packages/cross_file/** @ditman @mvanbeusekom packages/file_selector/** @ditman packages/google_maps_flutter/** @cyanglaz -packages/google_sign_in/** @cyanglaz @mehmetf packages/image_picker/** @cyanglaz packages/integration_test/** @dnfield -packages/in_app_purchase/** @mklim @cyanglaz @LHLL +packages/in_app_purchase/** @cyanglaz @LHLL packages/ios_platform_images/** @gaaclarke -packages/package_info/** @cyanglaz @matthew-carroll -packages/path_provider/** @matthew-carroll -packages/shared_preferences/** @matthew-carroll -packages/url_launcher/** @mklim -packages/video_player/** @iskakaushik @cyanglaz -packages/webview_flutter/** @amirh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b278b43e524f..6c4abd0cb516 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,26 @@ [![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)_ + +## Important note + +As of January 2021, we are no longer accepting non-critical PRs for plugins +for which there is a corresponding [Flutter Community Plus +Plugin](https://plus.fluttercommunity.dev/), as we hope in time to be able +to transition users to those versions of the plugins. If you have a PR for +something other than a critical issue (crashes, build failures, null safety, etc.) +for any of the following plugins, we encourage you to submit it +[there](https://github.com/fluttercommunity/plus_plugins/pulls) instead: +- `android_alarm_manager` +- `android_intent` +- `battery` +- `connectivity` +- `device_info` +- `package_info` +- `sensors` +- `share` +- `wifi_info_flutter` (corresponds to `network_info_plus`) ## Things you will need @@ -131,9 +150,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/README.md b/README.md index 42c64e1c6a50..b65c10e7f381 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ These plugins are also available on Please file any issues, bugs, or feature requests in the [main flutter repo](https://github.com/flutter/flutter/issues/new). +Issues pertaining to this repository are [labeled +"plugin"](https://github.com/flutter/flutter/issues?q=is%3Aopen+is%3Aissue+label%3Aplugin). + ## Contributing If you wish to contribute a new plugin to the Flutter ecosystem, please diff --git a/analysis_options.yaml b/analysis_options.yaml index b1261f36fac9..2b62a6a9e2b9 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -4,6 +4,10 @@ analyzer: # Ignore generated files - '**/*.g.dart' - 'lib/src/generated/*.dart' + - '**/*.mocks.dart' # Mockito @GenerateMocks + 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..10ad02a5ac43 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.0.0 + +* Migrate to null safety. + +## 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..5a8a55e43641 100644 --- a/packages/android_alarm_manager/README.md +++ b/packages/android_alarm_manager/README.md @@ -1,17 +1,10 @@ # 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. -**Please set your constraint to `android_alarm_manager: '>=0.4.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.4.y+z`. -Please use `android_alarm_manager: '>=0.4.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Getting Started After importing this plugin to your project as usual, add the following to your @@ -36,7 +29,7 @@ Next, within the `` tags, add: android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver" android:enabled="false"> - + @@ -109,18 +102,13 @@ Which must be reflected in the application's `AndroidManifest.xml`. E.g.: **Note:** Not calling `AlarmService.setPluginRegistrant` will result in an exception being thrown when an alarm eventually fires. -### Flutter Android Embedding V2 (Flutter Version >= 1.12) +### Flutter Android Embedding V2 For the Flutter Android Embedding V2, plugins are registered with the background isolate via reflection so `AlarmService.setPluginRegistrant` does not need to be called. -**NOTE: this plugin is not completely compatible with the V2 embedding on -Flutter versions < 1.12 as the background isolate will not automatically -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"> - + - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/android_alarm_manager/example/ios/Flutter/Debug.xcconfig b/packages/android_alarm_manager/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/android_alarm_manager/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/android_alarm_manager/example/ios/Flutter/Release.xcconfig b/packages/android_alarm_manager/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/android_alarm_manager/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/android_alarm_manager/example/ios/Runner.xcodeproj/project.pbxproj b/packages/android_alarm_manager/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 10f77b41d130..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* 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, ); }; }; - 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 */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - C952AD53387AE85A4AAC19D3 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 365DE79D3A08F3F6322AB7B4 /* libPods-Runner.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 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 = ""; }; - 192BD17BD81C291EF9467E75 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 365DE79D3A08F3F6322AB7B4 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 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 = ""; }; - 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 = ""; }; - 842A7CA20B55950D87F2A01A /* 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 = ""; }; - 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 = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - C952AD53387AE85A4AAC19D3 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1B44A04DB1D7DBDE7E239095 /* Pods */ = { - isa = PBXGroup; - children = ( - 192BD17BD81C291EF9467E75 /* Pods-Runner.debug.xcconfig */, - 842A7CA20B55950D87F2A01A /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 1B44A04DB1D7DBDE7E239095 /* Pods */, - B10ADDD1244B5A67F70F5F08 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - B10ADDD1244B5A67F70F5F08 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 365DE79D3A08F3F6322AB7B4 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9AC722C5D70651C49D7ECF80 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - E95CF7E4BD7CAFC3E0F4E1E2 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - 9AC722C5D70651C49D7ECF80 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - E95CF7E4BD7CAFC3E0F4E1E2 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.androidAlarmManagerExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.androidAlarmManagerExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/android_alarm_manager/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/android_alarm_manager/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/android_alarm_manager/example/ios/Runner/AppDelegate.h b/packages/android_alarm_manager/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bbf9cf4..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/android_alarm_manager/example/ios/Runner/AppDelegate.m b/packages/android_alarm_manager/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 59a72e90be12..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/android_alarm_manager/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/android_alarm_manager/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7c939..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/android_alarm_manager/example/ios/Runner/Base.lproj/Main.storyboard b/packages/android_alarm_manager/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/android_alarm_manager/example/ios/Runner/Info.plist b/packages/android_alarm_manager/example/ios/Runner/Info.plist deleted file mode 100644 index 1d076337d6f4..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - android_alarm_manager_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/android_alarm_manager/example/ios/Runner/main.m b/packages/android_alarm_manager/example/ios/Runner/main.m deleted file mode 100644 index dff6597e4513..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/android_alarm_manager/example/lib/main.dart b/packages/android_alarm_manager/example/lib/main.dart index b4d907c127da..dc3779c8cbe2 100644 --- a/packages/android_alarm_manager/example/lib/main.dart +++ b/packages/android_alarm_manager/example/lib/main.dart @@ -22,7 +22,7 @@ const String isolateName = 'isolate'; final ReceivePort port = ReceivePort(); /// Global [SharedPreferences] object. -SharedPreferences prefs; +late SharedPreferences prefs; Future main() async { // TODO(bkonyi): uncomment @@ -54,7 +54,7 @@ class AlarmManagerExampleApp extends StatelessWidget { } class _AlarmHomePage extends StatefulWidget { - _AlarmHomePage({Key key, this.title}) : super(key: key); + _AlarmHomePage({Key? key, required this.title}) : super(key: key); final String title; @override @@ -86,7 +86,7 @@ class _AlarmHomePageState extends State<_AlarmHomePage> { } // The background - static SendPort uiSendPort; + static SendPort? uiSendPort; // The callback for our alarm static Future callback() async { @@ -94,7 +94,7 @@ class _AlarmHomePageState extends State<_AlarmHomePage> { // Get the previous cached count and increment it. final prefs = await SharedPreferences.getInstance(); - int currentCount = prefs.getInt(countKey); + int currentCount = prefs.getInt(countKey) ?? 0; await prefs.setInt(countKey, currentCount + 1); // This will be null if we're running in the background. @@ -131,7 +131,7 @@ class _AlarmHomePageState extends State<_AlarmHomePage> { ), ], ), - RaisedButton( + ElevatedButton( child: Text( 'Schedule OneShot Alarm', ), @@ -140,7 +140,7 @@ class _AlarmHomePageState extends State<_AlarmHomePage> { await AndroidAlarmManager.oneShot( const Duration(seconds: 5), // Ensure we have a unique alarm ID. - Random().nextInt(pow(2, 31)), + Random().nextInt(pow(2, 31).toInt()), callback, exact: true, wakeup: true, diff --git a/packages/android_alarm_manager/example/pubspec.yaml b/packages/android_alarm_manager/example/pubspec.yaml index e636a246376b..b92f45c73d35 100644 --- a/packages/android_alarm_manager/example/pubspec.yaml +++ b/packages/android_alarm_manager/example/pubspec.yaml @@ -5,11 +5,16 @@ dependencies: flutter: sdk: flutter android_alarm_manager: + # When depending on this package from a real application you should use: + # android_alarm_manager: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - shared_preferences: ^0.5.6 + shared_preferences: ^2.0.0 integration_test: path: ../../integration_test - path_provider: ^1.3.1 + path_provider: ^2.0.0 dev_dependencies: espresso: ^0.0.1+3 @@ -17,11 +22,11 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: '>=2.12.0-259.9.beta <3.0.0' + flutter: ">=1.20.0" diff --git a/packages/android_alarm_manager/example/test_driver/integration_test.dart b/packages/android_alarm_manager/example/test_driver/integration_test.dart index ed54518d7d00..4e78d04fa971 100644 --- a/packages/android_alarm_manager/example/test_driver/integration_test.dart +++ b/packages/android_alarm_manager/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/android_alarm_manager/ios/Assets/.gitkeep b/packages/android_alarm_manager/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/android_alarm_manager/ios/Classes/AndroidAlarmManagerPlugin.h b/packages/android_alarm_manager/ios/Classes/AndroidAlarmManagerPlugin.h deleted file mode 100644 index 595fcf60fee1..000000000000 --- a/packages/android_alarm_manager/ios/Classes/AndroidAlarmManagerPlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// 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 - -@interface FLTAndroidAlarmManagerPlugin : NSObject -@end diff --git a/packages/android_alarm_manager/ios/Classes/AndroidAlarmManagerPlugin.m b/packages/android_alarm_manager/ios/Classes/AndroidAlarmManagerPlugin.m deleted file mode 100644 index 0aa4f2b2122d..000000000000 --- a/packages/android_alarm_manager/ios/Classes/AndroidAlarmManagerPlugin.m +++ /dev/null @@ -1,21 +0,0 @@ -// 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 "AndroidAlarmManagerPlugin.h" - -@implementation FLTAndroidAlarmManagerPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/android_alarm_manager" - binaryMessenger:[registrar messenger] - codec:[FlutterJSONMethodCodec sharedInstance]]; - FLTAndroidAlarmManagerPlugin* instance = [[FLTAndroidAlarmManagerPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - result(FlutterMethodNotImplemented); -} - -@end diff --git a/packages/android_alarm_manager/ios/android_alarm_manager.podspec b/packages/android_alarm_manager/ios/android_alarm_manager.podspec deleted file mode 100644 index 2b253878c1ea..000000000000 --- a/packages/android_alarm_manager/ios/android_alarm_manager.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'android_alarm_manager' - s.version = '0.0.1' - s.summary = 'Flutter Android Alarm Manager' - s.description = <<-DESC -A Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire. -This plugin a no-op on iOS. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager' } - s.documentation_url = 'https://pub.dev/packages/android_alarm_manager' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/android_alarm_manager/lib/android_alarm_manager.dart b/packages/android_alarm_manager/lib/android_alarm_manager.dart index b8afa134472c..218959bc4bcc 100644 --- a/packages/android_alarm_manager/lib/android_alarm_manager.dart +++ b/packages/android_alarm_manager/lib/android_alarm_manager.dart @@ -31,7 +31,7 @@ void _alarmManagerCallbackDispatcher() { // PluginUtilities.getCallbackFromHandle performs a lookup based on the // callback handle and returns a tear-off of the original callback. - final Function closure = PluginUtilities.getCallbackFromHandle(handle); + final Function? closure = PluginUtilities.getCallbackFromHandle(handle); if (closure == null) { print('Fatal: could not find callback'); @@ -56,7 +56,7 @@ void _alarmManagerCallbackDispatcher() { // A lambda that returns the current instant in the form of a [DateTime]. typedef DateTime _Now(); // A lambda that gets the handle for the given [callback]. -typedef CallbackHandle _GetCallbackHandle(Function callback); +typedef CallbackHandle? _GetCallbackHandle(Function callback); /// A Flutter plugin for registering Dart callbacks with the Android /// AlarmManager service. @@ -77,7 +77,7 @@ class AndroidAlarmManager { /// the plugin. @visibleForTesting static void setTestOverides( - {_Now now, _GetCallbackHandle getCallbackHandle}) { + {_Now? now, _GetCallbackHandle? getCallbackHandle}) { _now = (now ?? _now); _getCallbackHandle = (getCallbackHandle ?? _getCallbackHandle); } @@ -88,12 +88,12 @@ class AndroidAlarmManager { /// Returns a [Future] that resolves to `true` on success and `false` on /// failure. static Future initialize() async { - final CallbackHandle handle = + final CallbackHandle? handle = _getCallbackHandle(_alarmManagerCallbackDispatcher); if (handle == null) { return false; } - final bool r = await _channel.invokeMethod( + final bool? r = await _channel.invokeMethod( 'AlarmService.start', [handle.toRawHandle()]); return r ?? false; } @@ -207,11 +207,11 @@ class AndroidAlarmManager { assert(callback is Function() || callback is Function(int)); assert(id.bitLength < 32); final int startMillis = time.millisecondsSinceEpoch; - final CallbackHandle handle = _getCallbackHandle(callback); + final CallbackHandle? handle = _getCallbackHandle(callback); if (handle == null) { return false; } - final bool r = + final bool? r = await _channel.invokeMethod('Alarm.oneShotAt', [ id, alarmClock, @@ -222,7 +222,7 @@ class AndroidAlarmManager { rescheduleOnReboot, handle.toRawHandle(), ]); - return (r == null) ? false : r; + return r ?? false; } /// Schedules a repeating timer to run `callback` with period `duration`. @@ -262,7 +262,7 @@ class AndroidAlarmManager { Duration duration, int id, Function callback, { - DateTime startAt, + DateTime? startAt, bool exact = false, bool wakeup = false, bool rescheduleOnReboot = false, @@ -274,11 +274,11 @@ class AndroidAlarmManager { final int period = duration.inMilliseconds; final int first = startAt != null ? startAt.millisecondsSinceEpoch : now + period; - final CallbackHandle handle = _getCallbackHandle(callback); + final CallbackHandle? handle = _getCallbackHandle(callback); if (handle == null) { return false; } - final bool r = await _channel.invokeMethod( + final bool? r = await _channel.invokeMethod( 'Alarm.periodic', [ id, exact, @@ -288,7 +288,7 @@ class AndroidAlarmManager { rescheduleOnReboot, handle.toRawHandle() ]); - return (r == null) ? false : r; + return r ?? false; } /// Cancels a timer. @@ -299,8 +299,8 @@ class AndroidAlarmManager { /// Returns a [Future] that resolves to `true` on success and `false` on /// failure. static Future cancel(int id) async { - final bool r = + final bool? r = await _channel.invokeMethod('Alarm.cancel', [id]); - return (r == null) ? false : r; + return r ?? false; } } diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml index 9de2d8308419..3934de6f06fe 100644 --- a/packages/android_alarm_manager/pubspec.yaml +++ b/packages/android_alarm_manager/pubspec.yaml @@ -1,10 +1,7 @@ name: android_alarm_manager description: Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire. -# 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: 2.0.0 homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager dependencies: @@ -14,7 +11,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: plugin: @@ -24,5 +21,5 @@ flutter: pluginClass: AndroidAlarmManagerPlugin environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: '>=2.12.0-259.9.beta <3.0.0' + flutter: ">=1.20.0" diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index 5a3ba03b33eb..70926b4f4943 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -1,3 +1,13 @@ +## 2.0.0 + +* Migrate to null safety. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 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..7d097e465926 100644 --- a/packages/android_intent/README.md +++ b/packages/android_intent/README.md @@ -5,13 +5,6 @@ is Android. If the plugin is invoked on iOS, it will crash your app. In checked mode, we assert that the platform should be Android. -**Please set your constraint to `android_intent: '>=0.3.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.3.y+z`. -Please use `android_intent: '>=0.3.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - Use it by specifying action, category, data and extra arguments for the intent. It does not support returning the result of the launched activity. Sample usage: @@ -53,13 +46,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 +60,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/integration_test/android_intent_test.dart b/packages/android_intent/example/integration_test/android_intent_test.dart index 78a667b27a09..e11a5e4e4898 100644 --- a/packages/android_intent/example/integration_test/android_intent_test.dart +++ b/packages/android_intent/example/integration_test/android_intent_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 'dart:io'; import 'package:android_intent/android_intent.dart'; diff --git a/packages/android_intent/example/ios/Flutter/AppFrameworkInfo.plist b/packages/android_intent/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/android_intent/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/android_intent/example/ios/Flutter/Debug.xcconfig b/packages/android_intent/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/android_intent/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/android_intent/example/ios/Flutter/Release.xcconfig b/packages/android_intent/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/android_intent/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj b/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 430cec7ef2b5..000000000000 --- a/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* 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, ); }; }; - 3FC5CBD67A867C34C8CFD7E1 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ABB9ACA70E30025F77BB759 /* 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 */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 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 = ""; }; - 7ABB9ACA70E30025F77BB759 /* 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 = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9B21C620C27B8C2AF08BFA21 /* 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 = ""; }; - EFC3461395B2546568135556 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 3FC5CBD67A867C34C8CFD7E1 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 2C36A917BF8B34817D5A406D /* Pods */ = { - isa = PBXGroup; - children = ( - EFC3461395B2546568135556 /* Pods-Runner.debug.xcconfig */, - 9B21C620C27B8C2AF08BFA21 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 7423FCEB8AD9C632FAF625A3 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 7ABB9ACA70E30025F77BB759 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 2C36A917BF8B34817D5A406D /* Pods */, - 7423FCEB8AD9C632FAF625A3 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - ECD6A6833016AB689F7B8471 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 4B2738B48C3E53795176CD79 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 4B2738B48C3E53795176CD79 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - ECD6A6833016AB689F7B8471 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.androidIntentExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.androidIntentExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/android_intent/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/android_intent/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/android_intent/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/android_intent/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/android_intent/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/android_intent/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/android_intent/example/ios/Runner/AppDelegate.h b/packages/android_intent/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/android_intent/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,10 +0,0 @@ -// 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 -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/android_intent/example/ios/Runner/AppDelegate.m b/packages/android_intent/example/ios/Runner/AppDelegate.m deleted file mode 100644 index f08675707182..000000000000 --- a/packages/android_intent/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +0,0 @@ -// 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. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/android_intent/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/android_intent/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index ebf48f603974..000000000000 --- a/packages/android_intent/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/android_intent/example/ios/Runner/Base.lproj/Main.storyboard b/packages/android_intent/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/android_intent/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/android_intent/example/ios/Runner/Info.plist b/packages/android_intent/example/ios/Runner/Info.plist deleted file mode 100644 index 61ad692e0180..000000000000 --- a/packages/android_intent/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - android_intent_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/android_intent/example/ios/Runner/main.m b/packages/android_intent/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/android_intent/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// 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 -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} 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/example/pubspec.yaml b/packages/android_intent/example/pubspec.yaml index 84455d99e618..fd0e2d6b7844 100644 --- a/packages/android_intent/example/pubspec.yaml +++ b/packages/android_intent/example/pubspec.yaml @@ -5,6 +5,11 @@ dependencies: flutter: sdk: flutter android_intent: + # When depending on this package from a real application you should use: + # android_intent: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -12,12 +17,12 @@ dev_dependencies: path: ../../integration_test flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 # The following section is specific to Flutter. flutter: uses-material-design: true environment: - sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/android_intent/example/test_driver/integration_test.dart b/packages/android_intent/example/test_driver/integration_test.dart index 34483b996049..0378ec31ee3c 100644 --- a/packages/android_intent/example/test_driver/integration_test.dart +++ b/packages/android_intent/example/test_driver/integration_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 'dart:async'; import 'dart:convert'; import 'dart:io'; diff --git a/packages/android_intent/ios/Assets/.gitkeep b/packages/android_intent/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/android_intent/ios/Classes/AndroidIntentPlugin.h b/packages/android_intent/ios/Classes/AndroidIntentPlugin.h deleted file mode 100644 index 8810c13f61cf..000000000000 --- a/packages/android_intent/ios/Classes/AndroidIntentPlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// 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 - -@interface FLTAndroidIntentPlugin : NSObject -@end diff --git a/packages/android_intent/ios/Classes/AndroidIntentPlugin.m b/packages/android_intent/ios/Classes/AndroidIntentPlugin.m deleted file mode 100644 index d708adf8c1d0..000000000000 --- a/packages/android_intent/ios/Classes/AndroidIntentPlugin.m +++ /dev/null @@ -1,20 +0,0 @@ -// 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 "AndroidIntentPlugin.h" - -@implementation FLTAndroidIntentPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/android_intent" - binaryMessenger:[registrar messenger]]; - FLTAndroidIntentPlugin* instance = [[FLTAndroidIntentPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - result(FlutterMethodNotImplemented); -} - -@end diff --git a/packages/android_intent/ios/android_intent.podspec b/packages/android_intent/ios/android_intent.podspec deleted file mode 100644 index b3f9b6eb334f..000000000000 --- a/packages/android_intent/ios/android_intent.podspec +++ /dev/null @@ -1,24 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'android_intent' - s.version = '0.0.1' - s.summary = 'Android Intent Plugin for Flutter' - s.description = <<-DESC -This plugin allows Flutter apps to launch arbitrary intents when the platform is Android. -If the plugin is invoked on iOS, it will crash your app. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/android_intent' } - s.documentation_url = 'https://pub.dev/packages/android_intent' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end - 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..e02c7a270344 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 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 + meta: ^1.3.0 dev_dependencies: - test: ^1.3.0 - mockito: ^3.0.0 + test: ^1.16.3 + mockito: ^5.0.0-nullsafety.7 flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" 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..ae9e798c364d 100644 --- a/packages/battery/battery/CHANGELOG.md +++ b/packages/battery/battery/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.0.0 + +* Migrate to null safety. + +## 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..8482655771f2 100644 --- a/packages/battery/battery/example/lib/main.dart +++ b/packages/battery/battery/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; @@ -36,10 +36,10 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - Battery _battery = Battery(); + final Battery _battery = Battery(); - BatteryState _batteryState; - StreamSubscription _batteryStateSubscription; + BatteryState? _batteryState; + late StreamSubscription _batteryStateSubscription; @override void initState() { @@ -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/example/pubspec.yaml b/packages/battery/battery/example/pubspec.yaml index 4e7b9ef035eb..ea3d5d39ae14 100644 --- a/packages/battery/battery/example/pubspec.yaml +++ b/packages/battery/battery/example/pubspec.yaml @@ -5,6 +5,11 @@ dependencies: flutter: sdk: flutter battery: + # When depending on this package from a real application you should use: + # battery: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -12,11 +17,11 @@ dev_dependencies: sdk: flutter integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/battery/battery/integration_test/battery_test.dart b/packages/battery/battery/integration_test/battery_test.dart index ed7b6fe5a0e4..2b0e26967b6c 100644 --- a/packages/battery/battery/integration_test/battery_test.dart +++ b/packages/battery/battery/integration_test/battery_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:battery/battery.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/battery/battery/pubspec.yaml b/packages/battery/battery/pubspec.yaml index ec747190ac69..a987bd8c45a6 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: 2.0.0 flutter: plugin: @@ -16,20 +16,18 @@ flutter: dependencies: flutter: sdk: flutter - meta: ^1.0.5 - battery_platform_interface: ^1.0.0 + meta: ^1.3.0 + battery_platform_interface: ^2.0.0 dev_dependencies: - async: ^2.0.8 - test: ^1.3.0 - mockito: ^4.1.1 flutter_test: sdk: flutter - plugin_platform_interface: ^1.0.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" integration_test: path: ../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 + test: ^1.16.3 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/battery/battery/test/battery_test.dart b/packages/battery/battery/test/battery_test.dart index 5c789207d7eb..ff1bf1596250 100644 --- a/packages/battery/battery/test/battery_test.dart +++ b/packages/battery/battery/test/battery_test.dart @@ -4,15 +4,15 @@ import 'dart:async'; +import 'package:battery/battery.dart'; import 'package:battery_platform_interface/battery_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:test/test.dart'; -import 'package:battery/battery.dart'; -import 'package:mockito/mockito.dart'; +import 'package:test/fake.dart'; void main() { group('battery', () { - Battery battery; + late Battery battery; MockBatteryPlatform fakePlatform; setUp(() async { fakePlatform = MockBatteryPlatform(); @@ -30,7 +30,7 @@ void main() { }); } -class MockBatteryPlatform extends Mock +class MockBatteryPlatform extends Fake with MockPlatformInterfaceMixin implements BatteryPlatform { Future batteryLevel() async { diff --git a/packages/battery/battery_platform_interface/CHANGELOG.md b/packages/battery/battery_platform_interface/CHANGELOG.md index 6fadda91b380..2c51f2c2d352 100644 --- a/packages/battery/battery_platform_interface/CHANGELOG.md +++ b/packages/battery/battery_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.0 + +* 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..61edad6cc04b 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 dependencies: flutter: sdk: flutter - meta: ^1.1.8 - plugin_platform_interface: ^1.0.2 + meta: ^1.3.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 + mockito: ^5.0.0-nullsafety.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" 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..079aa1685bd5 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,143 @@ +## 0.8.0-nullsafety.3 + +* Updates the example code listed in the [README.md](README.md), so it runs without errors when you simply copy/ paste it into a Flutter App. + +## 0.8.0-nullsafety.2 + +* Solved delay when using the zoom feature on iOS. + +## 0.8.0-nullsafety.1 + +* Added a timeout to the pre-capture sequence on Android to prevent crashes when the camera cannot get a focus. + +## 0.8.0-nullsafety + +* Migrated to null safety. + +## 0.7.0+4 + +* Fix crash when taking picture with orientation lock + +## 0.7.0+3 + +* Clockwise rotation of focus point in android + +## 0.7.0+2 + +* Fix example reference in README. +* Revert compileSdkVersion back to 29 (from 30) as this is causing problems with add-to-app configurations. + +## 0.7.0+1 + +* Ensure communication from JAVA to Dart is done on the main UI thread. + +## 0.7.0 + +* BREAKING CHANGE: `CameraValue.aspectRatio` now returns `width / height` rather than `height / width`. [(commit)](https://github.com/flutter/plugins/commit/100c7470d4066b1d0f8f7e4ec6d7c943e736f970) + * Added support for capture orientation locking on Android and iOS. + * Fixed camera preview not rotating correctly on Android and iOS. + * Fixed camera preview sometimes appearing stretched on Android and iOS. + * Fixed videos & photos saving with the incorrect rotation on iOS. +* New Features: + * Adds auto focus support for Android and iOS implementations. [(commmit)](https://github.com/flutter/plugins/commit/71a831790220f898bf8120c8a23840ac6e742db5) + * Adds ImageFormat selection for ImageStream and Video(iOS only). [(commit)](https://github.com/flutter/plugins/commit/da1b4638b750a5ff832d7be86a42831c42c6d6c0) +* Bug Fixes: + * Fixes crash when taking a picture on iOS devices without flash. [(commit)](https://github.com/flutter/plugins/commit/831344490984b1feec007afc9c8595d80b6c13f4) + * Make sure the configured zoom scale is copied over to the final capture builder on Android. Fixes the issue where the preview is zoomed but the final picture is not. [(commit)](https://github.com/flutter/plugins/commit/5916f55664e1772a4c3f0c02c5c71fc11e491b76) + * Fixes crash with using inner camera on some Android devices. [(commit)](https://github.com/flutter/plugins/commit/980b674cb4020c1927917426211a87e275346d5e) + * Improved error feedback by differentiating between uninitialized and disposed camera controllers. [(commit)](https://github.com/flutter/plugins/commit/d0b7109f6b00a0eda03506fed2c74cc123ffc6f3) + * Fixes picture captures causing a crash on some Huawei devices. [(commit)](https://github.com/flutter/plugins/commit/6d18db83f00f4861ffe485aba2d1f8aa08845ce6) + +## 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..fb6144face9b 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. @@ -77,6 +79,7 @@ List cameras; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + cameras = await availableCameras(); runApp(CameraApp()); } @@ -92,7 +95,7 @@ class _CameraAppState extends State { @override void initState() { super.initState(); - controller = CameraController(cameras[0], ResolutionPreset.medium); + controller = CameraController(cameras[0], ResolutionPreset.max); controller.initialize().then((_) { if (!mounted) { return; @@ -112,15 +115,15 @@ class _CameraAppState extends State { if (!controller.value.isInitialized) { return Container(); } - return AspectRatio( - aspectRatio: - controller.value.aspectRatio, - child: CameraPreview(controller)); + return MaterialApp( + home: CameraPreview(controller), + ); } } + ``` -For a more elaborate usage example see [here](https://github.com/flutter/plugins/tree/master/packages/camera/example). +For a more elaborate usage example see [here](https://github.com/flutter/plugins/tree/master/packages/camera/camera/example). *Note*: This plugin is still under development, and some APIs might not be available yet. [Feedback welcome](https://github.com/flutter/flutter/issues) and 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..5169a3babb74 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.os.SystemClock; +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.embedding.engine.systemchannels.PlatformChannel; 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,38 +62,61 @@ 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"; + + /** Timeout for the pre-capture sequence. */ + private static final long PRECAPTURE_TIMEOUT_MS = 1000; + private final SurfaceTextureEntry flutterTexture; private final CameraManager cameraManager; - private final OrientationEventListener orientationEventListener; + private final DeviceOrientationManager deviceOrientationListener; private final boolean isFrontFacing; private final int sensorOrientation; private final String cameraName; 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 int currentOrientation = ORIENTATION_UNKNOWN; + private File videoRecordingFile; + 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; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + private long preCaptureStartTime; - // 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,32 +135,52 @@ public Camera( this.flutterTexture = flutterTexture; this.dartMessenger = dartMessenger; this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - orientationEventListener = - new OrientationEventListener(activity.getApplicationContext()) { - @Override - public void onOrientationChanged(int i) { - if (i == ORIENTATION_UNKNOWN) { - return; - } - // Convert the raw deg angle to the nearest multiple of 90. - currentOrientation = (int) Math.round(i / 90.0) * 90; - } - }; - 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 + this.applicationContext = activity.getApplicationContext(); + this.flashMode = FlashMode.auto; + this.exposureMode = ExposureMode.auto; + this.focusMode = FocusMode.auto; + this.exposureOffset = 0; + + 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)); + + deviceOrientationListener = + new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); + deviceOrientationListener.start(); + } + + 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 { @@ -124,20 +191,28 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { mediaRecorder = new MediaRecorderBuilder(recordingProfile, outputFilePath) .setEnableAudio(enableAudio) - .setMediaOrientation(getMediaOrientation()) + .setMediaOrientation( + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) .build(); } @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, @@ -147,16 +222,17 @@ public void onOpened(@NonNull CameraDevice device) { cameraDevice = device; try { 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 +244,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 +270,126 @@ 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); + } + } + + cameraRegions = new CameraRegions(getRegionBoundaries()); + + // 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,245 @@ 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; + boolean fatalFailure = false; + 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"; + fatalFailure = true; + break; + default: + reason = "Unknown reason"; + } + Log.w("Camera", "pictureCaptureCallback.onCaptureFailed(): " + reason); + if (fatalFailure) 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); + setPreCaptureStartTime(); + } + break; + case waitingPreCaptureReady: + if (aeState == null || aeState != CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) { + runPictureCapture(); + } else { + if (hitPreCaptureTimeout()) { + unlockAutoFocus(); + } + } + } + } + }; + + 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()); + captureBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + captureRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + captureBuilder.set( + CaptureRequest.JPEG_ORIENTATION, + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)); + 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 +649,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 +709,385 @@ 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(y, 1 - x); + // 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(y, 1 - x); + } + + // 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); + } + + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; + } + + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = 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 +1145,27 @@ private void setImageStreamImageAvailableListener(final EventChannel.EventSink i null); } + public void stopImageStream() throws CameraAccessException { + if (imageStreamReader != null) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + startPreview(); + } + + /** Sets the time the pre-capture sequence started. */ + private void setPreCaptureStartTime() { + preCaptureStartTime = SystemClock.elapsedRealtime(); + } + + /** + * Check if the timeout for the pre-capture sequence has been reached. + * + * @return true if the timeout is reached; otherwise false is returned. + */ + private boolean hitPreCaptureTimeout() { + return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; + } + private void closeCaptureSession() { if (cameraCaptureSession != null) { cameraCaptureSession.close(); @@ -498,14 +1198,6 @@ public void close() { public void dispose() { close(); flutterTexture.release(); - orientationEventListener.disable(); - } - - private int getMediaOrientation() { - final int sensorOrientationOffset = - (currentOrientation == ORIENTATION_UNKNOWN) - ? 0 - : (isFrontFacing) ? -currentOrientation : currentOrientation; - return (sensorOrientationOffset + sensorOrientation + 360) % 360; + deviceOrientationListener.stop(); } } 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..03993a3b51f9 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,8 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.CamcorderProfile; import android.util.Size; -import io.flutter.plugins.camera.Camera.ResolutionPreset; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.types.ResolutionPreset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -24,6 +29,59 @@ public final class CameraUtils { private CameraUtils() {} + static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) { + // Round to the nearest 90 degrees. + degrees = (int) (Math.round(degrees / 90.0) * 90) % 360; + // Determine the corresponding device orientation. + switch (degrees) { + case 90: + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + case 180: + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + case 270: + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + case 0: + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } + } + + static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) { + if (orientation == null) + throw new UnsupportedOperationException("Could not serialize null device orientation."); + switch (orientation) { + case PORTRAIT_UP: + return "portraitUp"; + case PORTRAIT_DOWN: + return "portraitDown"; + case LANDSCAPE_LEFT: + return "landscapeLeft"; + case LANDSCAPE_RIGHT: + return "landscapeRight"; + default: + throw new UnsupportedOperationException( + "Could not serialize device orientation: " + orientation.toString()); + } + } + + static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) { + if (orientation == null) + throw new UnsupportedOperationException("Could not deserialize null device orientation."); + switch (orientation) { + case "portraitUp": + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + case "portraitDown": + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + case "landscapeLeft": + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + case "landscapeRight": + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + default: + throw new UnsupportedOperationException( + "Could not deserialize device orientation: " + orientation); + } + } + static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { if (preset.ordinal() > ResolutionPreset.high.ordinal()) { preset = ResolutionPreset.high; 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..3892452892d9 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,136 @@ +// 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.os.Handler; +import android.os.Looper; import android.text.TextUtils; import androidx.annotation.Nullable; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; 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 cameraChannel; + @Nullable private MethodChannel deviceChannel; + + enum DeviceEventType { + ORIENTATION_CHANGED("orientation_changed"); + private final String method; - enum EventType { - ERROR, - CAMERA_CLOSING, + DeviceEventType(String method) { + this.method = method; + } } - 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; - } + enum CameraEventType { + ERROR("error"), + CLOSING("camera_closing"), + INITIALIZED("initialized"); - @Override - public void onCancel(Object arguments) { - eventSink = null; - } - }); + private final String method; + + CameraEventType(String method) { + this.method = method; + } + } + + DartMessenger(BinaryMessenger messenger, long cameraId) { + cameraChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); + deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device"); + } + + void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { + assert (orientation != null); + this.send( + DeviceEventType.ORIENTATION_CHANGED, + new HashMap() { + { + put("orientation", CameraUtils.serializeDeviceOrientation(orientation)); + } + }); + } + + 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( + CameraEventType.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(CameraEventType.CLOSING); + } + + void sendCameraErrorEvent(@Nullable String description) { + this.send( + CameraEventType.ERROR, + new HashMap() { + { + if (!TextUtils.isEmpty(description)) put("description", description); + } + }); + } + + void send(CameraEventType eventType) { + send(eventType, new HashMap<>()); } - void send(EventType eventType, @Nullable String description) { - if (eventSink == null) { + void send(CameraEventType eventType, Map args) { + if (cameraChannel == null) { return; } + new Handler(Looper.getMainLooper()) + .post( + new Runnable() { + @Override + public void run() { + cameraChannel.invokeMethod(eventType.method, args); + } + }); + } + + void send(DeviceEventType 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(DeviceEventType eventType, Map args) { + if (deviceChannel == null) { + return; } - eventSink.success(event); + new Handler(Looper.getMainLooper()) + .post( + new Runnable() { + @Override + public void run() { + deviceChannel.invokeMethod(eventType.method, args); + } + }); } } diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java new file mode 100644 index 000000000000..b2a504b629d6 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java @@ -0,0 +1,200 @@ +// 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; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.hardware.SensorManager; +import android.provider.Settings; +import android.view.Display; +import android.view.OrientationEventListener; +import android.view.Surface; +import android.view.WindowManager; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; + +class DeviceOrientationManager { + + private static final IntentFilter orientationIntentFilter = + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + + private final Activity activity; + private final DartMessenger messenger; + private final boolean isFrontFacing; + private final int sensorOrientation; + private PlatformChannel.DeviceOrientation lastOrientation; + private OrientationEventListener orientationEventListener; + private BroadcastReceiver broadcastReceiver; + + public DeviceOrientationManager( + Activity activity, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { + this.activity = activity; + this.messenger = messenger; + this.isFrontFacing = isFrontFacing; + this.sensorOrientation = sensorOrientation; + } + + public void start() { + startSensorListener(); + startUIListener(); + } + + public void stop() { + stopSensorListener(); + stopUIListener(); + } + + public int getMediaOrientation() { + return this.getMediaOrientation(this.lastOrientation); + } + + public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { + int angle = 0; + + // Fallback to device orientation when the orientation value is null + if (orientation == null) { + orientation = getUIOrientation(); + } + + switch (orientation) { + case PORTRAIT_UP: + angle = 0; + break; + case PORTRAIT_DOWN: + angle = 180; + break; + case LANDSCAPE_LEFT: + angle = 90; + break; + case LANDSCAPE_RIGHT: + angle = 270; + break; + } + if (isFrontFacing) angle *= -1; + return (angle + sensorOrientation + 360) % 360; + } + + private void startSensorListener() { + if (orientationEventListener != null) return; + orientationEventListener = + new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { + @Override + public void onOrientationChanged(int angle) { + if (!isSystemAutoRotationLocked()) { + PlatformChannel.DeviceOrientation newOrientation = calculateSensorOrientation(angle); + if (!newOrientation.equals(lastOrientation)) { + lastOrientation = newOrientation; + messenger.sendDeviceOrientationChangeEvent(newOrientation); + } + } + } + }; + if (orientationEventListener.canDetectOrientation()) { + orientationEventListener.enable(); + } + } + + private void startUIListener() { + if (broadcastReceiver != null) return; + broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (isSystemAutoRotationLocked()) { + PlatformChannel.DeviceOrientation orientation = getUIOrientation(); + if (!orientation.equals(lastOrientation)) { + lastOrientation = orientation; + messenger.sendDeviceOrientationChangeEvent(orientation); + } + } + } + }; + activity.registerReceiver(broadcastReceiver, orientationIntentFilter); + broadcastReceiver.onReceive(activity, null); + } + + private void stopSensorListener() { + if (orientationEventListener == null) return; + orientationEventListener.disable(); + orientationEventListener = null; + } + + private void stopUIListener() { + if (broadcastReceiver == null) return; + activity.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + private boolean isSystemAutoRotationLocked() { + return android.provider.Settings.System.getInt( + activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) + != 1; + } + + private PlatformChannel.DeviceOrientation getUIOrientation() { + final int rotation = getDisplay().getRotation(); + final int orientation = activity.getResources().getConfiguration().orientation; + + switch (orientation) { + case Configuration.ORIENTATION_PORTRAIT: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } else { + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + } + case Configuration.ORIENTATION_LANDSCAPE: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + } else { + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + } + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } + } + + private PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { + final int tolerance = 45; + angle += tolerance; + + // Orientation is 0 in the default orientation mode. This is portait-mode for phones + // and landscape for tablets. We have to compensate for this by calculating the default + // orientation, and apply an offset accordingly. + int defaultDeviceOrientation = getDeviceDefaultOrientation(); + if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { + angle += 90; + } + // Determine the orientation + angle = angle % 360; + return new PlatformChannel.DeviceOrientation[] { + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + } + [angle / 90]; + } + + private int getDeviceDefaultOrientation() { + Configuration config = activity.getResources().getConfiguration(); + int rotation = getDisplay().getRotation(); + if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) + && config.orientation == Configuration.ORIENTATION_LANDSCAPE) + || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) + && config.orientation == Configuration.ORIENTATION_PORTRAIT)) { + return Configuration.ORIENTATION_LANDSCAPE; + } else { + return Configuration.ORIENTATION_PORTRAIT; + } + } + + @SuppressWarnings("deprecation") + private Display getDisplay() { + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + } +} 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..aa7483f55679 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,16 +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. + package io.flutter.plugins.camera; import android.app.Activity; import android.hardware.camera2.CameraAccessException; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodCall; 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 +59,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 +80,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 +112,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 +130,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 { @@ -123,6 +263,72 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result 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 "lockCaptureOrientation": + { + PlatformChannel.DeviceOrientation orientation = + CameraUtils.deserializeDeviceOrientation(call.argument("orientation")); + + try { + camera.lockCaptureOrientation(orientation); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "unlockCaptureOrientation": + { + try { + camera.unlockCaptureOrientation(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } case "dispose": { if (camera != null) { @@ -157,7 +363,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..396f782a2a08 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -0,0 +1,96 @@ +// 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.os.Handler; +import android.os.Looper; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.MethodChannel; + +class PictureCaptureRequest { + + enum State { + idle, + focusing, + preCapture, + waitingPreCaptureReady, + capturing, + finished, + error, + } + + private final Runnable timeoutCallback = + new Runnable() { + @Override + public void run() { + error("captureTimeout", "Picture capture request timed out", state.toString()); + } + }; + + private final MethodChannel.Result result; + private final TimeoutHandler timeoutHandler; + private State state; + + public PictureCaptureRequest(MethodChannel.Result result) { + this(result, new TimeoutHandler()); + } + + public PictureCaptureRequest(MethodChannel.Result result, TimeoutHandler timeoutHandler) { + this.result = result; + this.state = State.idle; + this.timeoutHandler = timeoutHandler; + } + + public void setState(State state) { + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + this.state = state; + if (state != State.idle && state != State.finished && state != State.error) { + this.timeoutHandler.resetTimeout(timeoutCallback); + } else { + this.timeoutHandler.clearTimeout(timeoutCallback); + } + } + + 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"); + this.timeoutHandler.clearTimeout(timeoutCallback); + 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"); + this.timeoutHandler.clearTimeout(timeoutCallback); + result.error(errorCode, errorMessage, errorDetails); + state = State.error; + } + + static class TimeoutHandler { + private static final int REQUEST_TIMEOUT = 5000; + private final Handler handler; + + TimeoutHandler() { + this.handler = new Handler(Looper.getMainLooper()); + } + + public void resetTimeout(Runnable runnable) { + clearTimeout(runnable); + handler.postDelayed(runnable, REQUEST_TIMEOUT); + } + + public void clearTimeout(Runnable runnable) { + handler.removeCallbacks(runnable); + } + } +} 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/flutter_plugin_android_lifecycle/ios/Classes/FlutterAndroidLifecyclePlugin.h b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java similarity index 52% rename from packages/flutter_plugin_android_lifecycle/ios/Classes/FlutterAndroidLifecyclePlugin.h rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java index a554ce0500c6..1508dcefb293 100644 --- a/packages/flutter_plugin_android_lifecycle/ios/Classes/FlutterAndroidLifecyclePlugin.h +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java @@ -2,7 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import +package io.flutter.plugins.camera.types; -@interface FlutterAndroidLifecyclePlugin : NSObject -@end +// 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/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java new file mode 100644 index 000000000000..8026b6349aa1 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -0,0 +1,102 @@ +// 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 io.flutter.embedding.engine.systemchannels.PlatformChannel; +import org.junit.Test; + +public class CameraUtilsTest { + + @Test + public void serializeDeviceOrientation_serializes_correctly() { + assertEquals( + "portraitUp", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); + assertEquals( + "portraitDown", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); + assertEquals( + "landscapeLeft", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); + assertEquals( + "landscapeRight", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); + } + + @Test(expected = UnsupportedOperationException.class) + public void serializeDeviceOrientation_throws_for_null() { + CameraUtils.serializeDeviceOrientation(null); + } + + @Test + public void deserializeDeviceOrientation_deserializes_correctly() { + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.deserializeDeviceOrientation("portraitUp")); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.deserializeDeviceOrientation("portraitDown")); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.deserializeDeviceOrientation("landscapeLeft")); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.deserializeDeviceOrientation("landscapeRight")); + } + + @Test(expected = UnsupportedOperationException.class) + public void deserializeDeviceOrientation_throws_for_null() { + CameraUtils.deserializeDeviceOrientation(null); + } + + @Test + public void getDeviceOrientationFromDegrees_converts_correctly() { + // Portrait UP + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(0)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(315)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(44)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(-45)); + // Portrait DOWN + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(180)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(135)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(224)); + // Landscape LEFT + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(90)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(45)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(134)); + // Landscape RIGHT + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(270)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(225)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(314)); + } +} 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..e835b08f441a 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,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; 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.embedding.engine.systemchannels.PlatformChannel; 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 +53,57 @@ public void setUp() { } @Test - public void setsStreamHandler() { - assertNotNull(fakeBinaryMessenger.getMessageHandler()); + public void sendCameraErrorEvent_includesErrorDescriptions() { + dartMessenger.sendCameraErrorEvent("error description"); + + List sentMessages = fakeBinaryMessenger.getMessages(); + assertEquals(1, sentMessages.size()); + MethodCall call = decodeSentMessage(sentMessages.get(0)); + assertEquals("error", call.method); + assertEquals("error description", call.argument("description")); } @Test - public void send_handlesNullEventSinks() { - 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(0, sentMessages.size()); + assertEquals(1, sentMessages.size()); + 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 send_includesErrorDescriptions() { - initializeEventSink(); - - dartMessenger.send(DartMessenger.EventType.ERROR, "error description"); + public void sendCameraClosingEvent() { + dartMessenger.sendCameraClosingEvent(); 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("camera_closing", call.method); + assertNull(call.argument("description")); } @Test - public void sendCameraClosingEvent() { - initializeEventSink(); - - dartMessenger.sendCameraClosingEvent(); + public void sendDeviceOrientationChangedEvent() { + dartMessenger.sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation.PORTRAIT_UP); 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("orientation_changed", call.method); + assertEquals(call.argument("orientation"), "portraitUp"); } - 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..3252b3e111c4 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -0,0 +1,152 @@ +// 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.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +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 setState_resets_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); + req.setState(PictureCaptureRequest.State.focusing); + req.setState(PictureCaptureRequest.State.preCapture); + req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); + req.setState(PictureCaptureRequest.State.capturing); + verify(mockTimeoutHandler, times(4)).resetTimeout(any()); + verify(mockTimeoutHandler, never()).clearTimeout(any()); + } + + @Test + public void setState_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); + req.setState(PictureCaptureRequest.State.idle); + req.setState(PictureCaptureRequest.State.finished); + req = new PictureCaptureRequest(null, mockTimeoutHandler); + req.setState(PictureCaptureRequest.State.error); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler, times(3)).clearTimeout(any()); + } + + @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 finish_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); + req.finish("/test/path"); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + + @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 + public void error_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); + req.error("ERROR_CODE", "Error Message", null); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + + @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/Flutter/Debug.xcconfig b/packages/camera/camera/example/ios/Flutter/Debug.xcconfig index e8efba114687..b2f5fae9c254 100644 --- a/packages/camera/camera/example/ios/Flutter/Debug.xcconfig +++ b/packages/camera/camera/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/packages/camera/camera/example/ios/Flutter/Release.xcconfig b/packages/camera/camera/example/ios/Flutter/Release.xcconfig index 399e9340e6f6..88c29144c836 100644 --- a/packages/camera/camera/example/ios/Flutter/Release.xcconfig +++ b/packages/camera/camera/example/ios/Flutter/Release.xcconfig @@ -1,2 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj index 862ee64fb666..3f71bb69d6b6 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 */, @@ -159,7 +147,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - FE224661708E6DA2A0F8B952 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -181,6 +168,7 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = 7624MWN53C; }; }; }; @@ -229,7 +217,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; @@ -263,21 +251,6 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - FE224661708E6DA2A0F8B952 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -315,7 +288,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 +344,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -426,6 +397,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEVELOPMENT_TEAM = 7624MWN53C; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -447,6 +419,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/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..919434a6254f 100644 --- a/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/packages/camera/camera/example/ios/Runner/Info.plist b/packages/camera/camera/example/ios/Runner/Info.plist index f389a129e028..ff2e341a1803 100644 --- a/packages/camera/camera/example/ios/Runner/Info.plist +++ b/packages/camera/camera/example/ios/Runner/Info.plist @@ -39,6 +39,7 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 3ec6604ad788..6244aa5a8e37 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( @@ -130,32 +171,39 @@ class _CameraExampleHomeState extends State ), ); } else { - return AspectRatio( - aspectRatio: controller.value.aspectRatio, - child: CameraPreview(controller), + return Listener( + onPointerDown: (_) => _pointers++, + onPointerUp: (_) => _pointers--, + child: CameraPreview( + controller, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onScaleStart: _handleScaleStart, + onScaleUpdate: _handleScaleUpdate, + onTapDown: (details) => onViewFinderTap(details, constraints), + ); + }), + ), ); } } - /// 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,232 @@ 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, + ), + IconButton( + icon: Icon(controller?.value?.isCaptureOrientationLocked ?? false + ? Icons.screen_lock_rotation + : Icons.screen_rotation), + color: Colors.blue, + onPressed: controller != null + ? onCaptureOrientationLockButtonPressed + : 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 +550,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 +567,7 @@ class _CameraExampleHomeState extends State cameraDescription, ResolutionPreset.medium, enableAudio: enableAudio, + imageFormatGroup: ImageFormatGroup.jpeg, ); // If the controller is updated then update the UI. @@ -296,6 +580,16 @@ class _CameraExampleHomeState extends State try { await controller.initialize(); + await Future.wait([ + controller + .getMinExposureOffset() + .then((value) => _minAvailableExposureOffset = value), + controller + .getMaxExposureOffset() + .then((value) => _maxAvailableExposureOffset = value), + controller.getMaxZoomLevel().then((value) => _maxAvailableZoom = value), + controller.getMinZoomLevel().then((value) => _minAvailableZoom = value), + ]); } on CameraException catch (e) { _showCameraException(e); } @@ -306,29 +600,103 @@ 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 onCaptureOrientationLockButtonPressed() async { + if (controller != null) { + if (controller.value.isCaptureOrientationLocked) { + await controller.unlockCaptureOrientation(); + showInSnackBar('Capture orientation unlocked'); + } else { + await controller.lockCaptureOrientation(); + showInSnackBar( + 'Capture orientation locked to ${controller.value.lockedCaptureOrientation.toString().split('.').last}'); } + } + } + + 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 +714,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 +772,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 +821,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 +846,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..2a45fd69194c 100644 --- a/packages/camera/camera/example/pubspec.yaml +++ b/packages/camera/camera/example/pubspec.yaml @@ -3,6 +3,11 @@ description: Demonstrates how to use the camera plugin. dependencies: camera: + # When depending on this package from a real application you should use: + # camera: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ path_provider: ^0.5.0 flutter: @@ -22,5 +27,5 @@ flutter: uses-material-design: true environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.9.1+hotfix.4" diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 525c1286717a..c1770ff6d40b 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] @@ -17,8 +18,6 @@ @interface FLTSavePhotoDelegate : NSObject @property(readonly, nonatomic) NSString *path; @property(readonly, nonatomic) FlutterResult result; -@property(readonly, nonatomic) CMMotionManager *motionManager; -@property(readonly, nonatomic) AVCaptureDevicePosition cameraPosition; @end @interface FLTImageStreamHandler : NSObject @@ -44,17 +43,12 @@ @implementation FLTSavePhotoDelegate { FLTSavePhotoDelegate *selfReference; } -- initWithPath:(NSString *)path - result:(FlutterResult)result - motionManager:(CMMotionManager *)motionManager - cameraPosition:(AVCaptureDevicePosition)cameraPosition { +- initWithPath:(NSString *)path result:(FlutterResult)result { self = [super init]; NSAssert(self, @"super init cannot be nil"); _path = path; - _result = result; - _motionManager = motionManager; - _cameraPosition = cameraPosition; selfReference = self; + _result = result; return self; } @@ -69,50 +63,207 @@ - (void)captureOutput:(AVCapturePhotoOutput *)output _result(getFlutterError(error)); return; } + NSData *data = [AVCapturePhotoOutput JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer previewPhotoSampleBuffer:previewPhotoSampleBuffer]; - 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]; + bool success = [data writeToFile:_path atomically:YES]; + if (!success) { _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); return; } - _result(nil); + _result(_path); } -- (UIImageOrientation)getImageRotation { - float const threshold = 45.0; - BOOL (^isNearValue)(float value1, float value2) = ^BOOL(float value1, float value2) { - return fabsf(value1 - value2) < threshold; - }; - BOOL (^isNearValueABS)(float value1, float value2) = ^BOOL(float value1, float value2) { - return isNearValue(fabsf(value1), fabsf(value2)); - }; - float yxAtan = (atan2(_motionManager.accelerometerData.acceleration.y, - _motionManager.accelerometerData.acceleration.x)) * - 180 / M_PI; - if (isNearValue(-90.0, yxAtan)) { - return UIImageOrientationRight; - } else if (isNearValueABS(180.0, yxAtan)) { - return _cameraPosition == AVCaptureDevicePositionBack ? UIImageOrientationUp - : UIImageOrientationDown; - } else if (isNearValueABS(0.0, yxAtan)) { - return _cameraPosition == AVCaptureDevicePositionBack ? UIImageOrientationDown /*rotate 180* */ - : UIImageOrientationUp /*do not rotate*/; - } else if (isNearValue(90.0, yxAtan)) { - return UIImageOrientationLeft; - } - // If none of the above, then the device is likely facing straight down or straight up -- just - // pick something arbitrary - // TODO: Maybe use the UIInterfaceOrientation if in these scenarios - return UIImageOrientationUp; +- (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); } @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; + } +} + +static UIDeviceOrientation getUIDeviceOrientationForString(NSString *orientation) { + if ([orientation isEqualToString:@"portraitDown"]) { + return UIDeviceOrientationPortraitUpsideDown; + } else if ([orientation isEqualToString:@"landscapeLeft"]) { + return UIDeviceOrientationLandscapeRight; + } else if ([orientation isEqualToString:@"landscapeRight"]) { + return UIDeviceOrientationLandscapeLeft; + } else if ([orientation isEqualToString:@"portraitUp"]) { + return UIDeviceOrientationPortrait; + } else { + NSError *error = [NSError + errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : + [NSString stringWithFormat:@"Unknown device orientation %@", orientation] + }]; + @throw error; + } +} + +static NSString *getStringForUIDeviceOrientation(UIDeviceOrientation orientation) { + switch (orientation) { + case UIDeviceOrientationPortraitUpsideDown: + return @"portraitDown"; + case UIDeviceOrientationLandscapeRight: + return @"landscapeLeft"; + case UIDeviceOrientationLandscapeLeft: + return @"landscapeRight"; + case UIDeviceOrientationPortrait: + default: + return @"portraitUp"; + break; + }; +} + +// 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 +833,182 @@ - (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)lockCaptureOrientationWithResult:(FlutterResult)result + orientation:(NSString *)orientationStr { + UIDeviceOrientation orientation; + @try { + orientation = getUIDeviceOrientationForString(orientationStr); + } @catch (NSError *e) { + result(getFlutterError(e)); + return; + } + _lockedCaptureOrientation = orientation; + result(nil); +} + +- (void)unlockCaptureOrientationWithResult:(FlutterResult)result { + _lockedCaptureOrientation = UIDeviceOrientationUnknown; + 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 +1022,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 +1032,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.videoZoomFactor = zoom; + [_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; } } @@ -668,17 +1102,17 @@ - (BOOL)setupWriterForPath:(NSString *)path { [self setUpCaptureSessionForAudio]; } _videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL - fileType:AVFileTypeQuickTimeMovie + fileType:AVFileTypeMPEG4 error:&error]; NSParameterAssert(_videoWriter); if (error) { - _eventSink(@{@"event" : @"error", @"errorDescription" : error.description}); + [_methodChannel invokeMethod:errorMethod arguments:error.description]; return NO; } NSDictionary *videoSettings = [NSDictionary dictionaryWithObjectsAndKeys:AVVideoCodecH264, AVVideoCodecKey, - [NSNumber numberWithInt:_previewSize.height], AVVideoWidthKey, - [NSNumber numberWithInt:_previewSize.width], AVVideoHeightKey, + [NSNumber numberWithInt:_previewSize.width], AVVideoWidthKey, + [NSNumber numberWithInt:_previewSize.height], AVVideoHeightKey, nil]; _videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSettings]; @@ -690,6 +1124,14 @@ - (BOOL)setupWriterForPath:(NSString *)path { }]; NSParameterAssert(_videoWriterInput); + CGFloat rotationDegrees; + if (_lockedCaptureOrientation != UIDeviceOrientationUnknown) { + rotationDegrees = [self getRotationFromDeviceOrientation:_lockedCaptureOrientation]; + } else { + rotationDegrees = [self getRotationFromDeviceOrientation:[UIDevice currentDevice].orientation]; + } + + _videoWriterInput.transform = CGAffineTransformMakeRotation(rotationDegrees * M_PI / 180); _videoWriterInput.expectsMediaDataInRealTime = YES; // Add the audio input @@ -713,11 +1155,19 @@ - (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]; return YES; } + - (void)setUpCaptureSessionForAudio { NSError *error = nil; // Create a device input with the device and add it to the session. @@ -726,7 +1176,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,20 +1188,34 @@ - (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; } } } + +- (int)getRotationFromDeviceOrientation:(UIDeviceOrientation)orientation { + switch (orientation) { + case UIDeviceOrientationPortraitUpsideDown: + return 270; + case UIDeviceOrientationLandscapeRight: + return 180; + case UIDeviceOrientationLandscapeLeft: + return 0; + case UIDeviceOrientationPortrait: + default: + return 90; + }; +} + @end @interface CameraPlugin () @property(readonly, nonatomic) NSObject *registry; @property(readonly, nonatomic) NSObject *messenger; @property(readonly, nonatomic) FLTCam *camera; +@property(readonly, nonatomic) FlutterMethodChannel *deviceEventMethodChannel; @end @implementation CameraPlugin { @@ -772,9 +1236,36 @@ - (instancetype)initWithRegistry:(NSObject *)registry NSAssert(self, @"super init cannot be nil"); _registry = registry; _messenger = messenger; + [self initDeviceEventMethodChannel]; + [self startOrientationListener]; return self; } +- (void)initDeviceEventMethodChannel { + _deviceEventMethodChannel = + [FlutterMethodChannel methodChannelWithName:@"flutter.io/cameraPlugin/device" + binaryMessenger:_messenger]; +} + +- (void)startOrientationListener { + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(orientationChanged:) + name:UIDeviceOrientationDidChangeNotification + object:[UIDevice currentDevice]]; +} + +- (void)orientationChanged:(NSNotification *)note { + UIDevice *device = note.object; + [self sendDeviceOrientation:device.orientation]; +} + +- (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { + [_deviceEventMethodChannel + invokeMethod:@"orientation_changed" + arguments:@{@"orientation" : getStringForUIDeviceOrientation(orientation)}]; +} + - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if (_dispatchQueue == nil) { _dispatchQueue = dispatch_queue_create("io.flutter.camera.dispatchqueue", NULL); @@ -819,7 +1310,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 +1320,7 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re enableAudio:[enableAudio boolValue] dispatchQueue:_dispatchQueue error:&error]; + if (error) { result(getFlutterError(error)); } else { @@ -837,25 +1329,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 +1340,44 @@ - (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]), + }]; + [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; + [_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 +1385,57 @@ - (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 ([@"lockCaptureOrientation" isEqualToString:call.method]) { + [_camera lockCaptureOrientationWithResult:result orientation:call.arguments[@"orientation"]]; + } else if ([@"unlockCaptureOrientation" isEqualToString:call.method]) { + [_camera unlockCaptureOrientationWithResult:result]; + } 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..bb976d1c85fe --- /dev/null +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -0,0 +1,781 @@ +// 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'; +import 'package:quiver/core.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({ + required this.isInitialized, + this.errorDescription, + this.previewSize, + required this.isRecordingVideo, + required this.isTakingPicture, + required this.isStreamingImages, + required bool isRecordingPaused, + required this.flashMode, + required this.exposureMode, + required this.focusMode, + required this.exposurePointSupported, + required this.focusPointSupported, + required this.deviceOrientation, + this.lockedCaptureOrientation, + this.recordingOrientation, + }) : _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, + exposureMode: ExposureMode.auto, + exposurePointSupported: false, + focusMode: FocusMode.auto, + focusPointSupported: false, + deviceOrientation: DeviceOrientation.portraitUp, + ); + + /// 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.width / previewSize.height`. + /// + /// Can only be called when [initialize] is done. + double get aspectRatio => previewSize!.width / previewSize!.height; + + /// 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; + + /// The current device orientation. + final DeviceOrientation deviceOrientation; + + /// The currently locked capture orientation. + final DeviceOrientation? lockedCaptureOrientation; + + /// Whether the capture orientation is currently locked. + bool get isCaptureOrientationLocked => lockedCaptureOrientation != null; + + /// The orientation of the currently running video recording. + final DeviceOrientation? recordingOrientation; + + /// 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, + DeviceOrientation? deviceOrientation, + Optional? lockedCaptureOrientation, + Optional? recordingOrientation, + }) { + 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, + deviceOrientation: deviceOrientation ?? this.deviceOrientation, + lockedCaptureOrientation: lockedCaptureOrientation == null + ? this.lockedCaptureOrientation + : lockedCaptureOrientation.orNull, + recordingOrientation: recordingOrientation == null + ? this.recordingOrientation + : recordingOrientation.orNull, + ); + } + + @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, ' + 'deviceOrientation: $deviceOrientation, ' + 'lockedCaptureOrientation: $lockedCaptureOrientation, ' + 'recordingOrientation: $recordingOrientation)'; + } +} + +/// 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; + + /// The id of a camera that hasn't been initialized. + @visibleForTesting + static const int kUninitializedCameraId = -1; + int _cameraId = kUninitializedCameraId; + + bool _isDisposed = false; + StreamSubscription? _imageStreamSubscription; + FutureOr? _initCalled; + StreamSubscription? _deviceOrientationSubscription; + + /// 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(); + + _deviceOrientationSubscription = + CameraPlatform.instance.onDeviceOrientationChanged().listen((event) { + value = value.copyWith( + deviceOrientation: event.orientation, + ); + }); + + _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 ?? ImageFormatGroup.unknown, + ); + + 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 { + _throwIfNotInitialized("takePicture"); + 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); + _throwIfNotInitialized("startImageStream"); + 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); + _throwIfNotInitialized("stopImageStream"); + 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 { + _throwIfNotInitialized("startVideoRecording"); + 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, + recordingOrientation: Optional.fromNullable( + value.lockedCaptureOrientation ?? value.deviceOrientation)); + } 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 { + _throwIfNotInitialized("stopVideoRecording"); + 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, + recordingOrientation: Optional.absent(), + ); + 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 { + _throwIfNotInitialized("pauseVideoRecording"); + 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 { + _throwIfNotInitialized("resumeVideoRecording"); + 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() { + _throwIfNotInitialized("buildPreview"); + 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() { + _throwIfNotInitialized("getMaxZoomLevel"); + 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() { + _throwIfNotInitialized("getMinZoomLevel"); + 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) { + _throwIfNotInitialized("setZoomLevel"); + 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. + /// + /// Supplying a `null` value will reset the exposure point to it's default + /// 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 { + _throwIfNotInitialized("getMinExposureOffset"); + 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 { + _throwIfNotInitialized("getMaxExposureOffset"); + 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 { + _throwIfNotInitialized("getExposureOffsetStepSize"); + 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 { + _throwIfNotInitialized("setExposureOffset"); + // 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); + } + } + + /// Locks the capture orientation. + /// + /// If [orientation] is omitted, the current device orientation is used. + Future lockCaptureOrientation([DeviceOrientation? orientation]) async { + try { + await CameraPlatform.instance.lockCaptureOrientation( + _cameraId, orientation ?? value.deviceOrientation); + value = value.copyWith( + lockedCaptureOrientation: + Optional.fromNullable(orientation ?? value.deviceOrientation)); + } 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); + } + } + + /// Unlocks the capture orientation. + Future unlockCaptureOrientation() async { + try { + await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); + value = value.copyWith(lockedCaptureOrientation: Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the focus point for automatically determining the focus value. + /// + /// Supplying a `null` value will reset the focus point to it's default + /// 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; + } + unawaited(_deviceOrientationSubscription?.cancel()); + _isDisposed = true; + super.dispose(); + if (_initCalled != null) { + await _initCalled; + await CameraPlatform.instance.dispose(_cameraId); + } + } + + void _throwIfNotInitialized(String functionName) { + if (!value.isInitialized) { + throw CameraException( + 'Uninitialized CameraController', + '$functionName() was called on an uninitialized CameraController.', + ); + } + if (_isDisposed) { + throw CameraException( + 'Disposed CameraController', + '$functionName() was called on a disposed CameraController.', + ); + } + } +} diff --git a/packages/camera/camera/lib/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart similarity index 72% rename from packages/camera/camera/lib/camera_image.dart rename to packages/camera/camera/lib/src/camera_image.dart index cebc14873f52..46aa2a6e3091 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. /// @@ -22,7 +26,7 @@ class Plane { /// The distance between adjacent pixel samples on Android, in bytes. /// /// Will be `null` on iOS. - final int bytesPerPixel; + final int? bytesPerPixel; /// The row stride for this color plane, in bytes. final int bytesPerRow; @@ -30,38 +34,12 @@ class Plane { /// Height of the pixel buffer on iOS. /// /// Will be `null` on Android - final int height; + final int? height; /// Width of the pixel buffer on iOS. /// /// Will be `null` on Android. - 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, + final int? width; } /// Describes how pixels are represented in an image. @@ -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..f6d357b41b77 --- /dev/null +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -0,0 +1,66 @@ +// 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/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.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, {this.child}); + + /// The controller for the camera that the preview is shown for. + final CameraController controller; + + /// A widget to overlay on top of the camera preview + final Widget? child; + + @override + Widget build(BuildContext context) { + return controller.value.isInitialized + ? AspectRatio( + aspectRatio: _isLandscape() + ? controller.value.aspectRatio + : (1 / controller.value.aspectRatio), + child: Stack( + fit: StackFit.expand, + children: [ + RotatedBox( + quarterTurns: _getQuarterTurns(), + child: + CameraPlatform.instance.buildPreview(controller.cameraId), + ), + child ?? Container(), + ], + ), + ) + : Container(); + } + + DeviceOrientation _getApplicableOrientation() { + return controller.value.isRecordingVideo + ? controller.value.recordingOrientation! + : (controller.value.lockedCaptureOrientation ?? + controller.value.deviceOrientation); + } + + bool _isLandscape() { + return [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight] + .contains(_getApplicableOrientation()); + } + + int _getQuarterTurns() { + int platformOffset = defaultTargetPlatform == TargetPlatform.iOS ? 1 : 0; + Map turns = { + DeviceOrientation.portraitUp: 0, + DeviceOrientation.landscapeLeft: 1, + DeviceOrientation.portraitDown: 2, + DeviceOrientation.landscapeRight: 3, + }; + return turns[_getApplicableOrientation()]! + platformOffset; + } +} diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 7088faf227aa..2d620505def2 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -2,21 +2,26 @@ 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.8.0-nullsafety.3 homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: flutter: sdk: flutter + camera_platform_interface: ^2.0.0-nullsafety + + pedantic: ^1.10.0 + quiver: ^3.0.0-nullsafety.3 + dev_dependencies: - path_provider: ^0.5.0 - video_player: ^0.10.0 + video_player: ^2.0.0-nullsafety.7 flutter_test: sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.8.0 + mockito: ^5.0.0-nullsafety.5 + plugin_platform_interface: ^1.1.0-nullsafety.2 flutter: plugin: @@ -28,5 +33,5 @@ flutter: pluginClass: CameraPlugin 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.22.0" 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..57e3aeb36f3f --- /dev/null +++ b/packages/camera/camera/test/camera_image_stream_test.dart @@ -0,0 +1,205 @@ +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.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'startImageStream() was called on an 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.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'stopImageStream() was called on an 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..40ce29e363b1 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -1,10 +1,59 @@ // 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 mockOnDeviceOrientationChangedEvent => + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + +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,10 +81,1306 @@ 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.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'takePicture() was called on an 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.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'startVideoRecording() was called on an 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.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'getMaxZoomLevel() was called on an 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.code, + 'code', + 'Disposed CameraController', + ) + .having( + (error) => error.description, + 'description', + 'getMaxZoomLevel() was called on a disposed 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(CameraException( + 'TEST_ERROR', + 'This is a test error messge', + )); + + 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.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'getMinZoomLevel() was called on an 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.code, + 'code', + 'Disposed CameraController', + ) + .having( + (error) => error.description, + 'description', + 'getMinZoomLevel() was called on a disposed 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(CameraException( + 'TEST_ERROR', + 'This is a test error messge', + )); + + 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.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'setZoomLevel() was called on an 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.code, + 'code', + 'Disposed CameraController', + ) + .having( + (error) => error.description, + 'description', + 'setZoomLevel() was called on a disposed 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(CameraException( + 'TEST_ERROR', + 'This is a test error messge', + )); + + 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(); + + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) => Future.value(0.0)); + + 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( + CameraException( + 'TEST_ERROR', + 'This is a test error message', + ), + ); + + 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(); + + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) => Future.value(1.0)); + + 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( + CameraException( + 'TEST_ERROR', + 'This is a test error message', + ), + ); + + 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(); + + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) => Future.value(0.0)); + + await cameraController.getExposureOffsetStepSize(); + + verify(CameraPlatform.instance + .getExposureOffsetStepSize(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( + CameraException( + 'TEST_ERROR', + 'This is a test error message', + ), + ); + + 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); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 1.0)) + .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( + CameraException( + 'TEST_ERROR', + 'This is a test error message', + ), + ); + + 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); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.0)) + .thenAnswer((_) async => 0.0); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -1.0)) + .thenAnswer((_) async => 0.0); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 2.0)) + .thenAnswer((_) async => 0.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(0.0); + await cameraController.setExposureOffset(-1.0); + await cameraController.setExposureOffset(2.0); + + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.0)) + .called(1); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -1.0)) + .called(1); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 2.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.2); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 1.2); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 0.4); + + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -1.2)) + .thenAnswer((_) async => -1.2); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -0.8)) + .thenAnswer((_) async => -0.8); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -0.4)) + .thenAnswer((_) async => -0.4); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.0)) + .thenAnswer((_) async => 0.0); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.4)) + .thenAnswer((_) async => 0.4); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.8)) + .thenAnswer((_) async => 0.8); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 1.2)) + .thenAnswer((_) async => 1.2); + + await cameraController.setExposureOffset(1.2); + await cameraController.setExposureOffset(-1.2); + 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(2); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -0.8)) + .called(2); + 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); + }); + + test('lockCaptureOrientation() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.lockCaptureOrientation(); + expect(cameraController.value.lockedCaptureOrientation, + equals(DeviceOrientation.portraitUp)); + await cameraController + .lockCaptureOrientation(DeviceOrientation.landscapeRight); + expect(cameraController.value.lockedCaptureOrientation, + equals(DeviceOrientation.landscapeRight)); + + verify(CameraPlatform.instance.lockCaptureOrientation( + cameraController.cameraId, DeviceOrientation.portraitUp)) + .called(1); + verify(CameraPlatform.instance.lockCaptureOrientation( + cameraController.cameraId, DeviceOrientation.landscapeRight)) + .called(1); + }); + + test( + 'lockCaptureOrientation() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance.lockCaptureOrientation( + cameraController.cameraId, DeviceOrientation.portraitUp)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('unlockCaptureOrientation() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.unlockCaptureOrientation(); + expect(cameraController.value.lockedCaptureOrientation, equals(null)); + + verify(CameraPlatform.instance + .unlockCaptureOrientation(cameraController.cameraId)) + .called(1); + }); + + test( + 'unlockCaptureOrientation() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .unlockCaptureOrientation(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.unlockCaptureOrientation(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); }); } +class MockCameraPlatform extends Mock + with MockPlatformInterfaceMixin + implements CameraPlatform { + @override + Future initializeCamera( + int? cameraId, { + ImageFormatGroup? imageFormatGroup = ImageFormatGroup.unknown, + }) async => + super.noSuchMethod(Invocation.method( + #initializeCamera, + [cameraId], + { + #imageFormatGroup: imageFormatGroup, + }, + )); + + @override + Future dispose(int? cameraId) async { + return super.noSuchMethod(Invocation.method(#dispose, [cameraId])); + } + + @override + Future> availableCameras() => + Future.value(mockAvailableCameras); + + @override + Future createCamera( + CameraDescription description, + ResolutionPreset? resolutionPreset, { + bool enableAudio = false, + }) => + 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 + Stream onDeviceOrientationChanged() => + Stream.value(mockOnDeviceOrientationChangedEvent); + + @override + Future takePicture(int cameraId) => mockPlatformException + ? throw PlatformException(code: 'foo', message: 'bar') + : Future.value(mockTakePicture); + + @override + Future prepareForVideoRecording() async => + super.noSuchMethod(Invocation.method(#prepareForVideoRecording, null)); + + @override + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) => + Future.value(mockVideoRecordingXFile); + + @override + Future lockCaptureOrientation( + int? cameraId, DeviceOrientation? orientation) async => + super.noSuchMethod( + Invocation.method(#lockCaptureOrientation, [cameraId, orientation])); + + @override + Future unlockCaptureOrientation(int? cameraId) async => super + .noSuchMethod(Invocation.method(#unlockCaptureOrientation, [cameraId])); + + @override + Future getMaxZoomLevel(int? cameraId) async => super.noSuchMethod( + Invocation.method(#getMaxZoomLevel, [cameraId]), + returnValue: 1.0, + ); + + @override + Future getMinZoomLevel(int? cameraId) async => super.noSuchMethod( + Invocation.method(#getMinZoomLevel, [cameraId]), + returnValue: 0.0, + ); + + @override + Future setZoomLevel(int? cameraId, double? zoom) async => + super.noSuchMethod(Invocation.method(#setZoomLevel, [cameraId, zoom])); + + @override + Future setFlashMode(int? cameraId, FlashMode? mode) async => + super.noSuchMethod(Invocation.method(#setFlashMode, [cameraId, mode])); + + @override + Future setExposureMode(int? cameraId, ExposureMode? mode) async => + super.noSuchMethod(Invocation.method(#setExposureMode, [cameraId, mode])); + + @override + Future setExposurePoint(int? cameraId, Point? point) async => + super.noSuchMethod( + Invocation.method(#setExposurePoint, [cameraId, point])); + + @override + Future getMinExposureOffset(int? cameraId) async => + super.noSuchMethod( + Invocation.method(#getMinExposureOffset, [cameraId]), + returnValue: 0.0, + ); + + @override + Future getMaxExposureOffset(int? cameraId) async => + super.noSuchMethod( + Invocation.method(#getMaxExposureOffset, [cameraId]), + returnValue: 1.0, + ); + + @override + Future getExposureOffsetStepSize(int? cameraId) async => + super.noSuchMethod( + Invocation.method(#getExposureOffsetStepSize, [cameraId]), + returnValue: 1.0, + ); + + @override + Future setExposureOffset(int? cameraId, double? offset) async => + super.noSuchMethod( + Invocation.method(#setExposureOffset, [cameraId, offset]), + returnValue: 1.0, + ); +} + class MockCameraDescription extends CameraDescription { + /// Creates a new camera description with the given properties. + MockCameraDescription() + : super( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ); + @override CameraLensDirection get lensDirection => CameraLensDirection.back; 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..de7971d963c0 --- /dev/null +++ b/packages/camera/camera/test/camera_value_test.dart @@ -0,0 +1,141 @@ +// 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/services.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, + focusMode: FocusMode.auto, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, + focusPointSupported: 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); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect( + cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp); + }); + + 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, ExposureMode.auto); + expect(cameraValue.exposurePointSupported, false); + expect(cameraValue.focusMode, FocusMode.auto); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.lockedCaptureOrientation, null); + expect(cameraValue.recordingOrientation, null); + }); + + 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.focusMode, FocusMode.auto); + expect(cameraValue.exposureMode, ExposureMode.auto); + expect(cameraValue.exposurePointSupported, false); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.lockedCaptureOrientation, null); + expect(cameraValue.recordingOrientation, null); + }); + + test('Has aspectRatio after setting size', () { + var cv = const CameraValue.uninitialized(); + var cameraValue = + cv.copyWith(isInitialized: true, previewSize: Size(20, 10)); + + expect(cameraValue.aspectRatio, 2.0); + }); + + 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, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, + ); + + 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, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)'); + }); + }); +} 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..fdbd9a18f29c --- /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({ + required String channelName, + this.delay, + required 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..f7f78197d204 --- /dev/null +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -0,0 +1,48 @@ +## 2.0.0 + +- Stable null safety release. + +## 1.6.0 + +- Added VideoRecordedEvent to support ending a video recording in the native implementation. + +## 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..20aa41c5ce40 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart @@ -0,0 +1,283 @@ +// 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, + this.focusMode, + this.focusPointSupported, + ) : 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; +} + +/// An event fired when a video has finished recording. +class VideoRecordedEvent extends CameraEvent { + /// XFile of the recorded video. + final XFile file; + + /// Maximum duration of the recorded video. + final Duration? maxVideoDuration; + + /// Build a VideoRecordedEvent triggered from the camera with the `cameraId`. + /// + /// The `file` represents the file of the video. + /// The `maxVideoDuration` shows if a maxVideoDuration shows if a maximum + /// video duration was set. + VideoRecordedEvent(int cameraId, this.file, this.maxVideoDuration) + : super(cameraId); + + /// Converts the supplied [Map] to an instance of the [VideoRecordedEvent] + /// class. + VideoRecordedEvent.fromJson(Map json) + : file = XFile(json['path']), + maxVideoDuration = json['maxVideoDuration'] != null + ? Duration(milliseconds: json['maxVideoDuration'] as int) + : null, + super(json['cameraId']); + + /// Converts the [VideoRecordedEvent] instance into a [Map] instance that can be + /// serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + 'path': file.path, + 'maxVideoDuration': maxVideoDuration?.inMilliseconds + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + super == other && + other is VideoRecordedEvent && + runtimeType == other.runtimeType && + maxVideoDuration == other.maxVideoDuration; + + @override + int get hashCode => + super.hashCode ^ file.hashCode ^ maxVideoDuration.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..9f7f723bcd79 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -0,0 +1,507 @@ +// 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/foundation.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'); + + if (cameras == null) { + return []; + } + + 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 = false, + }) async { + try { + final 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 = ImageFormatGroup.unknown, + }) { + _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 { + if (_channels.containsKey(cameraId)) { + final cameraChannel = _channels[cameraId]; + cameraChannel?.setMethodCallHandler(null); + _channels.remove(cameraId); + } + + await _channel.invokeMethod( + 'dispose', + {'cameraId': 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 onVideoRecordedEvent(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 { + final path = await _channel.invokeMethod( + 'takePicture', + {'cameraId': cameraId}, + ); + + if (path == null) { + throw CameraException( + 'INVALID_PATH', + 'The platform "$defaultTargetPlatform" did not return a path while reporting success. The platform should always return a valid path or report an error.', + ); + } + + 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 { + final path = await _channel.invokeMethod( + 'stopVideoRecording', + {'cameraId': cameraId}, + ); + + if (path == null) { + throw CameraException( + 'INVALID_PATH', + 'The platform "$defaultTargetPlatform" did not return a path while reporting success. The platform should always return a valid path or report an error.', + ); + } + + 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) async { + final minExposureOffset = await _channel.invokeMethod( + 'getMinExposureOffset', + {'cameraId': cameraId}, + ); + + return minExposureOffset!; + } + + @override + Future getMaxExposureOffset(int cameraId) async { + final maxExposureOffset = await _channel.invokeMethod( + 'getMaxExposureOffset', + {'cameraId': cameraId}, + ); + + return maxExposureOffset!; + } + + @override + Future getExposureOffsetStepSize(int cameraId) async { + final stepSize = await _channel.invokeMethod( + 'getExposureOffsetStepSize', + {'cameraId': cameraId}, + ); + + return stepSize!; + } + + @override + Future setExposureOffset(int cameraId, double offset) async { + final appliedOffset = await _channel.invokeMethod( + 'setExposureOffset', + { + 'cameraId': cameraId, + 'offset': offset, + }, + ); + + return appliedOffset!; + } + + @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) async { + final maxZoomLevel = await _channel.invokeMethod( + 'getMaxZoomLevel', + {'cameraId': cameraId}, + ); + + return maxZoomLevel!; + } + + @override + Future getMinZoomLevel(int cameraId) async { + final minZoomLevel = await _channel.invokeMethod( + 'getMinZoomLevel', + {'cameraId': cameraId}, + ); + + return minZoomLevel!; + } + + @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 'video_recorded': + cameraEventStreamController.add(VideoRecordedEvent( + cameraId, + XFile(call.arguments['path']), + call.arguments['maxVideoDuration'] != null + ? Duration(milliseconds: call.arguments['maxVideoDuration']) + : null, + )); + 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..39a17e43dc0f --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -0,0 +1,247 @@ +// 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. + /// + /// This method returns an empty list when no cameras are available. + 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 = false, + }) { + 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 = ImageFormatGroup.unknown, + }) { + 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 camera finished recording a video + Stream onVideoRecordedEvent(int cameraId) { + throw UnimplementedError('onCameraTimeLimitReached() 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. + /// With [maxVideoDuration] set the video is returned in a [VideoRecordedEvent] + /// through the [onVideoRecordedEvent] stream when the set duration is reached. + 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. + /// + /// Supplying `null` for the [point] argument will result in resetting to the + /// original exposure point value. + 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. + /// + /// Supplying `null` for the [point] argument will result in resetting to the + /// original focus point value. + 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..9707bd34d0d2 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart @@ -0,0 +1,56 @@ +// 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({ + required this.name, + required this.lensDirection, + required 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..59f3e4e6647e --- /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..836f53826479 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart @@ -0,0 +1,36 @@ +// 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) { + 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) { + 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..8da2a90fa858 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart @@ -0,0 +1,36 @@ +// 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) { + 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) { + 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..10897073dc5c --- /dev/null +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -0,0 +1,24 @@ +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: 2.0.0 + +dependencies: + flutter: + sdk: flutter + meta: ^1.3.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" + cross_file: ^0.3.1 + stream_transform: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + async: ^2.5.0 + pedantic: ^1.10.0 + +environment: + sdk: '>=2.12.0-259.9.beta <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..f663db9776d1 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -0,0 +1,419 @@ +// 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/services.dart'; +import 'package:flutter_test/flutter_test.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( + '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, DeviceOrientation.portraitUp), + 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( + CameraDescription( + name: 'back', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of initializeCamera() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.initializeCamera(1), + 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, FlashMode.auto), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setExposureMode() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setExposureMode(1, ExposureMode.auto), + 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, 2.0), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setFocusMode() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setFocusMode(1, FocusMode.auto), + 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 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..5248d34c46b9 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -0,0 +1,928 @@ +// 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', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0), + ResolutionPreset.high, + ); + + // Assert + expect(cameraMockChannel.log, [ + isMethodCall( + 'create', + arguments: { + 'cameraName': 'Test', + 'resolutionPreset': 'high', + 'enableAudio': false + }, + ), + ]); + 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', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + 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', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + 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', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + 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', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + 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', () { + late MethodChannelCamera camera; + late 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', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + 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', () { + late MethodChannelCamera camera; + late 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', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + 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..fdbd9a18f29c --- /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({ + required String channelName, + this.delay, + required 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/connectivity/connectivity/CHANGELOG.md b/packages/connectivity/connectivity/CHANGELOG.md index 72b6d9b6513a..2f471890695a 100644 --- a/packages/connectivity/connectivity/CHANGELOG.md +++ b/packages/connectivity/connectivity/CHANGELOG.md @@ -1,3 +1,17 @@ +## 3.0.1 + +* Migrate tests to null safety. + +## 3.0.0 + +* Migrate to null safety. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Android: Cleanup the NetworkCallback object when a connectivity stream is cancelled + +## 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..6395dc38c3ae 100644 --- a/packages/connectivity/connectivity/example/pubspec.yaml +++ b/packages/connectivity/connectivity/example/pubspec.yaml @@ -5,18 +5,24 @@ dependencies: flutter: sdk: flutter connectivity: + # When depending on this package from a real application you should use: + # connectivity: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: flutter_driver: sdk: flutter + test: ^1.16.3 integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" 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..3aec6274f4b9 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.1 flutter: plugin: @@ -20,22 +20,24 @@ flutter: 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 + meta: ^1.3.0 + connectivity_platform_interface: ^2.0.0 + #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 + # connectivity_for_web: ^0.3.0 dev_dependencies: flutter_test: sdk: flutter flutter_driver: sdk: flutter + test: ^1.16.3 integration_test: path: ../../integration_test - mockito: ^4.1.1 - plugin_platform_interface: ^1.0.0 - pedantic: ^1.8.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <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..c95d0862444f 100644 --- a/packages/connectivity/connectivity/test/connectivity_test.dart +++ b/packages/connectivity/connectivity/test/connectivity_test.dart @@ -2,13 +2,11 @@ // 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:connectivity/connectivity.dart'; import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:mockito/mockito.dart'; +import 'package:test/fake.dart'; const ConnectivityResult kCheckConnectivityResult = ConnectivityResult.wifi; const LocationAuthorizationStatus kRequestLocationResult = @@ -18,8 +16,8 @@ const LocationAuthorizationStatus kGetLocationResult = void main() { group('Connectivity', () { - Connectivity connectivity; - MockConnectivityPlatform fakePlatform; + late Connectivity connectivity; + late MockConnectivityPlatform fakePlatform; setUp(() async { fakePlatform = MockConnectivityPlatform(); ConnectivityPlatform.instance = fakePlatform; @@ -33,7 +31,7 @@ void main() { }); } -class MockConnectivityPlatform extends Mock +class MockConnectivityPlatform extends Fake with MockPlatformInterfaceMixin implements ConnectivityPlatform { Future checkConnectivity() async { diff --git a/packages/connectivity/connectivity_for_web/ios/connectivity_for_web.podspec b/packages/connectivity/connectivity_for_web/ios/connectivity_for_web.podspec deleted file mode 100644 index 75b891c56533..000000000000 --- a/packages/connectivity/connectivity_for_web/ios/connectivity_for_web.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint connectivity_web.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'connectivity_for_web' - s.version = '0.1.0' - s.summary = 'No-op implementation of connectivity web plugin to avoid build issues on iOS' - s.description = <<-DESC -temp fake connectivity_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_for_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/connectivity/connectivity_macos/CHANGELOG.md b/packages/connectivity/connectivity_macos/CHANGELOG.md index 87910374bb8b..7031e48318dd 100644 --- a/packages/connectivity/connectivity_macos/CHANGELOG.md +++ b/packages/connectivity/connectivity_macos/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.2.0 + +* Remove placeholder Dart file. +* Update Dart SDK constraint for compatibility with null safety. + +## 0.1.0+8 + +* Update Flutter SDK constraint. + ## 0.1.0+7 * Remove unused `test` dependency. diff --git a/packages/connectivity/connectivity_macos/README.md b/packages/connectivity/connectivity_macos/README.md index 464f7d79ccd1..6974fd1fcc7e 100644 --- a/packages/connectivity/connectivity_macos/README.md +++ b/packages/connectivity/connectivity_macos/README.md @@ -2,13 +2,6 @@ The macos implementation of [`connectivity`]. -**Please set your constraint to `connectivity_macos: '>=0.1.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.1.y+z`. -Please use `connectivity_macos: '>=0.1.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package @@ -29,4 +22,3 @@ dependencies: ``` Refer to the `connectivity` [documentation](https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity) for more details. - diff --git a/packages/connectivity/connectivity_macos/example/ios/Flutter/AppFrameworkInfo.plist b/packages/connectivity/connectivity_macos/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Flutter/Debug.xcconfig b/packages/connectivity/connectivity_macos/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/connectivity/connectivity_macos/example/ios/Flutter/Release.xcconfig b/packages/connectivity/connectivity_macos/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/project.pbxproj b/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index e497d093be56..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* 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, ); }; }; - 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 */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - EB0BA966000B5C35B13186D7 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C80D49AFD183103034E444C2 /* libPods-Runner.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 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 = ""; }; - 3173C764DD180BE02EB51E47 /* 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 = ""; }; - 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 = ""; }; - 69D903F0A9A7C636EE803AF8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 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 = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C80D49AFD183103034E444C2 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - EB0BA966000B5C35B13186D7 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 89F516DEFCBF79E39D2885C2 /* Frameworks */ = { - isa = PBXGroup; - children = ( - C80D49AFD183103034E444C2 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - 8ECC1C323F60D5498EEC2315 /* Pods */ = { - isa = PBXGroup; - children = ( - 69D903F0A9A7C636EE803AF8 /* Pods-Runner.debug.xcconfig */, - 3173C764DD180BE02EB51E47 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 8ECC1C323F60D5498EEC2315 /* Pods */, - 89F516DEFCBF79E39D2885C2 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 3BAF367E8BACBC7576CEE653 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 6A2F146AD353BE7A0C3E797E /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 3BAF367E8BACBC7576CEE653 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 6A2F146AD353BE7A0C3E797E /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.connectivityExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.connectivityExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/connectivity/connectivity_macos/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/AppDelegate.h b/packages/connectivity/connectivity_macos/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,10 +0,0 @@ -// 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 -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/AppDelegate.m b/packages/connectivity/connectivity_macos/example/ios/Runner/AppDelegate.m deleted file mode 100644 index f08675707182..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +0,0 @@ -// 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. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/connectivity/connectivity_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index ebf48f603974..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Base.lproj/Main.storyboard b/packages/connectivity/connectivity_macos/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Info.plist b/packages/connectivity/connectivity_macos/example/ios/Runner/Info.plist deleted file mode 100644 index babbd80f1619..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/Info.plist +++ /dev/null @@ -1,53 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - connectivity_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - NSLocationAlwaysAndWhenInUseUsageDescription - This app requires accessing your location information all the time to get wi-fi information. - NSLocationWhenInUseUsageDescription - This app requires accessing your location information when the app is in foreground to get wi-fi information. - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/main.m b/packages/connectivity/connectivity_macos/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// 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 -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/connectivity/connectivity_macos/example/lib/main.dart b/packages/connectivity/connectivity_macos/example/lib/main.dart index 4ad30972679a..07746ed0f722 100644 --- a/packages/connectivity/connectivity_macos/example/lib/main.dart +++ b/packages/connectivity/connectivity_macos/example/lib/main.dart @@ -7,7 +7,7 @@ import 'dart:async'; import 'dart:io'; -import 'package:connectivity/connectivity.dart'; +import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -40,9 +40,9 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, this.title}) : super(key: key); - final String title; + final String? title; @override _MyHomePageState createState() => _MyHomePageState(); @@ -50,8 +50,8 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { String _connectionStatus = 'Unknown'; - final Connectivity _connectivity = Connectivity(); - StreamSubscription _connectivitySubscription; + final ConnectivityPlatform _connectivity = ConnectivityPlatform.instance; + 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; + late ConnectivityResult result; // Platform messages may fail, so we use a try/catch PlatformException. try { result = await _connectivity.checkConnectivity(); @@ -100,7 +100,7 @@ class _MyHomePageState extends State { Future _updateConnectionStatus(ConnectivityResult result) async { switch (result) { case ConnectivityResult.wifi: - String wifiName, wifiBSSID, wifiIP; + String? wifiName, wifiBSSID, wifiIP; try { if (Platform.isIOS) { diff --git a/packages/connectivity/connectivity_macos/example/pubspec.yaml b/packages/connectivity/connectivity_macos/example/pubspec.yaml index 041c3b364370..61cf16854d8b 100644 --- a/packages/connectivity/connectivity_macos/example/pubspec.yaml +++ b/packages/connectivity/connectivity_macos/example/pubspec.yaml @@ -4,8 +4,13 @@ description: Demonstrates how to use the connectivity plugin. dependencies: flutter: sdk: flutter - connectivity: any + connectivity_platform_interface: ^2.0.0 connectivity_macos: + # When depending on this package from a real application you should use: + # connectivity_macos: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -13,11 +18,11 @@ dev_dependencies: sdk: flutter integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.10.0" diff --git a/packages/connectivity/connectivity_macos/example/test_driver/integration_test/connectivity_test.dart b/packages/connectivity/connectivity_macos/example/test_driver/integration_test/connectivity_test.dart index 54a67337285a..3abe491d193b 100644 --- a/packages/connectivity/connectivity_macos/example/test_driver/integration_test/connectivity_test.dart +++ b/packages/connectivity/connectivity_macos/example/test_driver/integration_test/connectivity_test.dart @@ -2,19 +2,20 @@ // 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 'package:integration_test/integration_test.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:connectivity/connectivity.dart'; +import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('Connectivity test driver', () { - Connectivity _connectivity; + ConnectivityPlatform _connectivity; setUpAll(() async { - _connectivity = Connectivity(); + _connectivity = ConnectivityPlatform.instance; }); testWidgets('test connectivity result', (WidgetTester tester) async { diff --git a/packages/connectivity/connectivity_macos/ios/connectivity_macos.podspec b/packages/connectivity/connectivity_macos/ios/connectivity_macos.podspec deleted file mode 100644 index a941a16327f3..000000000000 --- a/packages/connectivity/connectivity_macos/ios/connectivity_macos.podspec +++ /dev/null @@ -1,21 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'connectivity_macos' - s.version = '0.0.1' - s.summary = 'No-op implementation of the connectivity desktop plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of connectivity_macos to avoid build issues on iOS. - See https://github.com/flutter/flutter/issues/39659 - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_macos' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end \ No newline at end of file diff --git a/packages/connectivity/connectivity_macos/lib/connectivity_macos.dart b/packages/connectivity/connectivity_macos/lib/connectivity_macos.dart deleted file mode 100644 index 7be7b143ca79..000000000000 --- a/packages/connectivity/connectivity_macos/lib/connectivity_macos.dart +++ /dev/null @@ -1,3 +0,0 @@ -// Analyze will fail if there is no main.dart file. This file should -// be removed once an example app has been added to connectivity_macos. -// https://github.com/flutter/flutter/issues/51007 diff --git a/packages/connectivity/connectivity_macos/pubspec.yaml b/packages/connectivity/connectivity_macos/pubspec.yaml index 2ab493072f8c..860b16497bc6 100644 --- a/packages/connectivity/connectivity_macos/pubspec.yaml +++ b/packages/connectivity/connectivity_macos/pubspec.yaml @@ -1,9 +1,6 @@ name: connectivity_macos 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 homepage: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_macos flutter: @@ -13,12 +10,12 @@ flutter: pluginClass: ConnectivityPlugin environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter dev_dependencies: - pedantic: ^1.8.0 + pedantic: ^1.10.0 diff --git a/packages/connectivity/connectivity_platform_interface/CHANGELOG.md b/packages/connectivity/connectivity_platform_interface/CHANGELOG.md index e45f8f7e4d99..0b26cd52c9e9 100644 --- a/packages/connectivity/connectivity_platform_interface/CHANGELOG.md +++ b/packages/connectivity/connectivity_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.0 + +* 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..1e89972dd816 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 dependencies: flutter: sdk: flutter - meta: ^1.0.5 - plugin_platform_interface: ^1.0.1 + meta: ^1.3.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <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 new file mode 100644 index 000000000000..94bf4b29322a --- /dev/null +++ b/packages/cross_file/CHANGELOG.md @@ -0,0 +1,30 @@ +## 0.3.1 + +* Fix nullability of `XFileBase`'s `path` and `name` to match the + implementations to avoid potential analyzer issues. + +## 0.3.0 + +* Migrated package to null-safety. +* **breaking change** According to our unit tests, the API should be backwards-compatible. Some relevant changes were made, however: + * Web: `lastModified` returns the epoch time as a default value, to maintain the `Future` return type (and not `null`) + +## 0.2.1 + +* Prepare for breaking `package:http` change. + +## 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. diff --git a/packages/cross_file/LICENSE b/packages/cross_file/LICENSE new file mode 100644 index 000000000000..2c91f1438173 --- /dev/null +++ b/packages/cross_file/LICENSE @@ -0,0 +1,25 @@ +Copyright 2020 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/cross_file/README.md b/packages/cross_file/README.md new file mode 100644 index 000000000000..65bd41896184 --- /dev/null +++ b/packages/cross_file/README.md @@ -0,0 +1,34 @@ +# cross_file + +An abstraction to allow working with files across multiple platforms. + +# Usage + +Import `package:cross/cross_info.dart`, instantiate a `CrossFile` +using a path or byte array and use its methods and properties to +access the file and its metadata. + +Example: + +```dart +import 'package:cross_file/cross_file.dart'; + +final file = CrossFile('assets/hello.txt'); + +print('File information:'); +print('- Path: ${file.path}'); +print('- Name: ${file.name}'); +print('- MIME type: ${file.mimeType}'); + +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.dev/packages/cross_file). + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](http://flutter.io/). + +For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). \ No newline at end of file diff --git a/packages/cross_file/lib/cross_file.dart b/packages/cross_file/lib/cross_file.dart new file mode 100644 index 000000000000..a3e2873e670d --- /dev/null +++ b/packages/cross_file/lib/cross_file.dart @@ -0,0 +1,5 @@ +// 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/x_file.dart'; diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/base.dart b/packages/cross_file/lib/src/types/base.dart similarity index 81% rename from packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/base.dart rename to packages/cross_file/lib/src/types/base.dart index 7ea050ff28db..4522b7343c9b 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/base.dart +++ b/packages/cross_file/lib/src/types/base.dart @@ -1,20 +1,24 @@ +// 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:convert'; import 'dart:typed_data'; -/// The interface for a XFile. +/// The interface for a CrossFile. /// -/// A XFile is a container that wraps the path of a selected +/// A CrossFile 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); + /// Construct a CrossFile + XFileBase(String? path); - /// Save the XFile at the indicated file path. - void saveTo(String path) async { + /// Save the CrossFile at the indicated file path. + Future saveTo(String path) { throw UnimplementedError('saveTo has not been implemented.'); } @@ -26,7 +30,7 @@ abstract class XFileBase { /// /// 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. + /// byte getters in the CrossFile instance instead. String get path { throw UnimplementedError('.path has not been implemented.'); } @@ -39,7 +43,7 @@ abstract class XFileBase { } /// For web, it may be necessary for a file to know its MIME type. - String get mimeType { + String? get mimeType { throw UnimplementedError('.mimeType has not been implemented.'); } @@ -71,11 +75,11 @@ abstract class XFileBase { /// 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]) { + Stream openRead([int? start, int? end]) { throw UnimplementedError('openRead() has not been implemented.'); } - /// Get the last-modified time for the XFile + /// Get the last-modified time for the CrossFile Future lastModified() { throw UnimplementedError('openRead() has not been implemented.'); } diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart new file mode 100644 index 000000000000..203ab5d82e12 --- /dev/null +++ b/packages/cross_file/lib/src/types/html.dart @@ -0,0 +1,135 @@ +// 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:convert'; +import 'dart:html'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +import './base.dart'; +import '../web_helpers/web_helpers.dart'; + +/// A CrossFile that works on web. +/// +/// It wraps the bytes of a selected file. +class XFile extends XFileBase { + late String path; + + final String? mimeType; + final Uint8List? _data; + final int? _length; + final String name; + final DateTime? _lastModified; + + late Element _target; + + final CrossFileTestOverrides? _overrides; + + bool get _hasTestOverrides => _overrides != null; + + /// Construct a CrossFile 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, + String? name, + int? length, + Uint8List? bytes, + DateTime? lastModified, + @visibleForTesting CrossFileTestOverrides? overrides, + }) : _data = bytes, + _length = length, + _overrides = overrides, + _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0), + name = name ?? '', + super(path); + + /// Construct an CrossFile from its data + XFile.fromData( + Uint8List bytes, { + this.mimeType, + String? name, + int? length, + DateTime? lastModified, + String? path, + @visibleForTesting CrossFileTestOverrides? overrides, + }) : _data = bytes, + _length = length, + _overrides = overrides, + _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0), + name = name ?? '', + super(path) { + if (path == null) { + final blob = (mimeType == null) ? Blob([bytes]) : Blob([bytes], mimeType); + this.path = Url.createObjectUrl(blob); + } else { + this.path = path; + } + } + + @override + Future lastModified() async => Future.value(_lastModified); + + Future get _bytes async { + if (_data != null) { + return Future.value(UnmodifiableUint8ListView(_data!)); + } + + // We can force 'response' to be a byte buffer by passing responseType: + ByteBuffer? response = + (await HttpRequest.request(path, responseType: 'arraybuffer')).response; + + return response?.asUint8List() ?? Uint8List(0); + } + + @override + Future length() async => _length ?? (await _bytes).length; + + @override + Future readAsString({Encoding encoding = utf8}) async { + return encoding.decode(await _bytes); + } + + @override + Future readAsBytes() async => 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 CrossFile at the location indicated by path. + /// For the web implementation, the path variable is ignored. + Future 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 CrossFileTestOverrides + final AnchorElement element = _hasTestOverrides + ? _overrides!.createAnchorElement(this.path, this.name) as AnchorElement + : 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 CrossFileTestOverrides { + /// For overriding the creation of the file input element. + Element Function(String href, String suggestedName) createAnchorElement; + + /// Default constructor for overrides + CrossFileTestOverrides({required this.createAnchorElement}); +} diff --git a/packages/cross_file/lib/src/types/interface.dart b/packages/cross_file/lib/src/types/interface.dart new file mode 100644 index 000000000000..122f3d1d9364 --- /dev/null +++ b/packages/cross_file/lib/src/types/interface.dart @@ -0,0 +1,58 @@ +// 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:typed_data'; +import 'package:meta/meta.dart'; + +import './base.dart'; + +/// A CrossFile 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 CrossFile 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 CrossFileTestOverrides? overrides, + }) : super(path) { + throw UnimplementedError( + 'CrossFile is not available in your current platform.'); + } + + /// Construct a CrossFile object from its data + XFile.fromData( + Uint8List bytes, { + String? mimeType, + String? name, + int? length, + DateTime? lastModified, + String? path, + @visibleForTesting CrossFileTestOverrides? overrides, + }) : super(path) { + throw UnimplementedError( + 'CrossFile is not available in your current platform.'); + } +} + +/// Overrides some functions of CrossFile for testing purposes +@visibleForTesting +class CrossFileTestOverrides { + /// For overriding the creation of the file input element. + dynamic Function(String href, String suggestedName) createAnchorElement; + + /// Default constructor for overrides + CrossFileTestOverrides({required this.createAnchorElement}); +} diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/io.dart b/packages/cross_file/lib/src/types/io.dart similarity index 68% rename from packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/io.dart rename to packages/cross_file/lib/src/types/io.dart index 753732df2811..6eafaf0ce0cc 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/io.dart +++ b/packages/cross_file/lib/src/types/io.dart @@ -1,39 +1,43 @@ +// 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:convert'; import 'dart:io'; import 'dart:typed_data'; import './base.dart'; -/// A XFile backed by a dart:io File. +/// A CrossFile backed by a dart:io File. class XFile extends XFileBase { final File _file; - final String mimeType; - final DateTime _lastModified; - int _length; + final String? mimeType; + final DateTime? _lastModified; + int? _length; - final Uint8List _bytes; + final Uint8List? _bytes; - /// Construct a XFile object backed by a dart:io File. + /// Construct a CrossFile object backed by a dart:io File. XFile( String path, { this.mimeType, - String name, - int length, - Uint8List bytes, - DateTime lastModified, + String? name, + int? length, + Uint8List? bytes, + DateTime? lastModified, }) : _file = File(path), _bytes = null, _lastModified = lastModified, super(path); - /// Construct an XFile from its data + /// Construct an CrossFile from its data XFile.fromData( Uint8List bytes, { this.mimeType, - String path, - String name, - int length, - DateTime lastModified, + String? path, + String? name, + int? length, + DateTime? lastModified, }) : _bytes = bytes, _file = File(path ?? ''), _length = length, @@ -53,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(); @@ -80,7 +84,7 @@ class XFile extends XFileBase { @override Future readAsString({Encoding encoding = utf8}) { if (_bytes != null) { - return Future.value(String.fromCharCodes(_bytes)); + return Future.value(String.fromCharCodes(_bytes!)); } return _file.readAsString(encoding: encoding); } @@ -93,13 +97,13 @@ class XFile extends XFileBase { return _file.readAsBytes(); } - Stream _getBytes(int start, int end) async* { - final bytes = _bytes; + 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]) { + Stream openRead([int? start, int? end]) { if (_bytes != null) { return _getBytes(start, end); } else { diff --git a/packages/cross_file/lib/src/web_helpers/web_helpers.dart b/packages/cross_file/lib/src/web_helpers/web_helpers.dart new file mode 100644 index 000000000000..a963e9933f99 --- /dev/null +++ b/packages/cross_file/lib/src/web_helpers/web_helpers.dart @@ -0,0 +1,38 @@ +// 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:html'; + +/// Create anchor element with download attribute +AnchorElement createAnchorElement(String href, String suggestedName) { + final element = AnchorElement(href: href); + + if (suggestedName == null) { + element.download = 'download'; + } else { + element.download = suggestedName; + } + + return element; +} + +/// Add an element to a container and click it +void addElementToContainerAndClick(Element container, Element element) { + // Add the element and click it + // All previous elements will be removed before adding the new one + container.children.add(element); + element.click(); +} + +/// Initializes a DOM container where we can host elements. +Element ensureInitialized(String id) { + var target = querySelector('#${id}'); + if (target == null) { + final Element targetElement = Element.tag('flt-x-file')..id = id; + + querySelector('body')!.children.add(targetElement); + target = targetElement; + } + return target; +} diff --git a/packages/cross_file/lib/src/x_file.dart b/packages/cross_file/lib/src/x_file.dart new file mode 100644 index 000000000000..6136bff39f36 --- /dev/null +++ b/packages/cross_file/lib/src/x_file.dart @@ -0,0 +1,7 @@ +// 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 'types/interface.dart' + if (dart.library.html) 'types/html.dart' + if (dart.library.io) 'types/io.dart'; diff --git a/packages/cross_file/pubspec.yaml b/packages/cross_file/pubspec.yaml new file mode 100644 index 000000000000..66d3f46a84e3 --- /dev/null +++ b/packages/cross_file/pubspec.yaml @@ -0,0 +1,18 @@ +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.3.1 + +dependencies: + flutter: + sdk: flutter + meta: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.10.0 + +environment: + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.22.0" diff --git a/packages/file_selector/file_selector_platform_interface/test/assets/hello.txt b/packages/cross_file/test/assets/hello.txt similarity index 100% rename from packages/file_selector/file_selector_platform_interface/test/assets/hello.txt rename to packages/cross_file/test/assets/hello.txt diff --git a/packages/file_selector/file_selector_platform_interface/test/x_file_html_test.dart b/packages/cross_file/test/x_file_html_test.dart similarity index 77% rename from packages/file_selector/file_selector_platform_interface/test/x_file_html_test.dart rename to packages/cross_file/test/x_file_html_test.dart index f888a0486ca7..a271aa1f1525 100644 --- a/packages/file_selector/file_selector_platform_interface/test/x_file_html_test.dart +++ b/packages/cross_file/test/x_file_html_test.dart @@ -9,12 +9,10 @@ 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'; +import 'package:cross_file/cross_file.dart'; final String expectedStringContents = 'Hello, world!'; -final Uint8List bytes = utf8.encode(expectedStringContents); +final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents)); final html.File textFile = html.File([bytes], 'hello.txt'); final String textFileUrl = html.Url.createObjectUrl(textFile); @@ -58,15 +56,15 @@ void main() { }); group('saveTo(..)', () { - final String xFileDomElementId = '__x_file_dom_element'; + final String CrossFileDomElementId = '__x_file_dom_element'; - group('XFile saveTo(..)', () { + group('CrossFile saveTo(..)', () { test('creates a DOM container', () async { XFile file = XFile.fromData(bytes); await file.saveTo(''); - final container = querySelector('#${xFileDomElementId}'); + final container = html.querySelector('#${CrossFileDomElementId}'); expect(container, isNotNull); }); @@ -76,20 +74,20 @@ void main() { await file.saveTo('path'); - final container = querySelector('#${xFileDomElementId}'); - final AnchorElement element = container?.children?.firstWhere( - (element) => element.tagName == 'A', - orElse: () => null); + final container = html.querySelector('#${CrossFileDomElementId}'); + final html.AnchorElement element = + container?.children.firstWhere((element) => element.tagName == 'A') + as html.AnchorElement; - expect(element, isNotNull); + // if element is not found, the `firstWhere` call will throw StateError. expect(element.href, file.path); expect(element.download, file.name); }); test('anchor element is clicked', () async { - final mockAnchor = AnchorElement(); + final mockAnchor = html.AnchorElement(); - XFileTestOverrides overrides = XFileTestOverrides( + CrossFileTestOverrides overrides = CrossFileTestOverrides( createAnchorElement: (_, __) => mockAnchor, ); diff --git a/packages/file_selector/file_selector_platform_interface/test/x_file_io_test.dart b/packages/cross_file/test/x_file_io_test.dart similarity index 81% rename from packages/file_selector/file_selector_platform_interface/test/x_file_io_test.dart rename to packages/cross_file/test/x_file_io_test.dart index b669324462b2..94ac81c4cac4 100644 --- a/packages/file_selector/file_selector_platform_interface/test/x_file_io_test.dart +++ b/packages/cross_file/test/x_file_io_test.dart @@ -9,28 +9,19 @@ 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/'; +import 'package:cross_file/cross_file.dart'; + +final pathPrefix = + Directory.current.path.endsWith('test') ? './assets/' : './test/assets/'; final path = pathPrefix + 'hello.txt'; final String expectedStringContents = 'Hello, world!'; -final Uint8List bytes = utf8.encode(expectedStringContents); +final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents)); final File textFile = File(path); final String textFilePath = textFile.path; void main() { group('Create with a path', () { - final file = XFile(textFilePath); + final XFile file = XFile(textFilePath); test('Can be read as a string', () async { expect(await file.readAsString(), equals(expectedStringContents)); diff --git a/packages/device_info/device_info/CHANGELOG.md b/packages/device_info/device_info/CHANGELOG.md index 06b327f4a198..f849131eff19 100644 --- a/packages/device_info/device_info/CHANGELOG.md +++ b/packages/device_info/device_info/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.0.0 + +* Migrate to null safety. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 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..bc7d00ef87f0 100644 --- a/packages/device_info/device_info/example/pubspec.yaml +++ b/packages/device_info/device_info/example/pubspec.yaml @@ -5,6 +5,11 @@ dependencies: flutter: sdk: flutter device_info: + # When depending on this package from a real application you should use: + # device_info: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -12,11 +17,11 @@ dev_dependencies: sdk: flutter integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0<3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" 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..0196338223a6 100644 --- a/packages/device_info/device_info/pubspec.yaml +++ b/packages/device_info/device_info/pubspec.yaml @@ -2,7 +2,7 @@ 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 +version: 2.0.0 flutter: plugin: @@ -16,14 +16,13 @@ flutter: dependencies: flutter: sdk: flutter - device_info_platform_interface: ^1.0.0 - + device_info_platform_interface: ^2.0.0 dev_dependencies: - test: ^1.3.0 + test: ^1.16.3 flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0<3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <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..23e9bc770c95 100644 --- a/packages/device_info/device_info_platform_interface/CHANGELOG.md +++ b/packages/device_info/device_info_platform_interface/CHANGELOG.md @@ -1,3 +1,13 @@ +## 2.0.0 + +* Migrate to null safety. +* Make `baseOS`, `previewSdkInt`, and `securityPatch` nullable types. +* Remove default values for non-nullable types. + +## 1.0.2 + +- Update Flutter SDK constraint. + ## 1.0.1 - Documentation typo fixed. diff --git a/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart b/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart index 808b7adf9dc7..2dd41dcc580f 100644 --- a/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart +++ b/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart @@ -7,10 +7,8 @@ import 'dart:async'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'method_channel/method_channel_device_info.dart'; - import 'model/android_device_info.dart'; import 'model/ios_device_info.dart'; - export 'model/android_device_info.dart'; export 'model/ios_device_info.dart'; diff --git a/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart b/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart index 7bd02e97436d..331f718989ce 100644 --- a/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart +++ b/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart @@ -1,8 +1,11 @@ +// 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 'package:flutter/services.dart'; import 'package:meta/meta.dart'; - import 'package:device_info_platform_interface/device_info_platform_interface.dart'; /// An implementation of [DeviceInfoPlatform] that uses method channels. @@ -13,16 +16,15 @@ class MethodChannelDeviceInfo extends DeviceInfoPlatform { // Method channel for Android devices Future androidInfo() async { - return AndroidDeviceInfo.fromMap( - (await channel.invokeMethod('getAndroidDeviceInfo')) - .cast(), - ); + return AndroidDeviceInfo.fromMap((await channel + .invokeMapMethod('getAndroidDeviceInfo')) ?? + {}); } // Method channel for iOS devices Future iosInfo() async { return IosDeviceInfo.fromMap( - (await channel.invokeMethod('getIosDeviceInfo')).cast(), - ); + (await channel.invokeMapMethod('getIosDeviceInfo')) ?? + {}); } } 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..c5210ab10f50 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); @@ -38,39 +38,63 @@ class AndroidDeviceInfo { final AndroidBuildVersion version; /// The name of the underlying board, like "goldfish". + /// + /// The value is an empty String if it is not available. final String board; /// The system bootloader version number. + /// + /// The value is an empty String if it is not available. final String bootloader; /// The consumer-visible brand with which the product/hardware will be associated, if any. + /// + /// The value is an empty String if it is not available. final String brand; /// The name of the industrial design. + /// + /// The value is an empty String if it is not available. final String device; /// A build ID string meant for displaying to the user. + /// + /// The value is an empty String if it is not available. final String display; /// A string that uniquely identifies this build. + /// + /// The value is an empty String if it is not available. final String fingerprint; /// The name of the hardware (from the kernel command line or /proc). + /// + /// The value is an empty String if it is not available. final String hardware; /// Hostname. + /// + /// The value is an empty String if it is not available. final String host; /// Either a changelist number, or a label like "M4-rc20". + /// + /// The value is an empty String if it is not available. final String id; /// The manufacturer of the product/hardware. + /// + /// The value is an empty String if it is not available. final String manufacturer; /// The end-user-visible name for the end product. + /// + /// The value is an empty String if it is not available. final String model; /// The name of the overall product. + /// + /// The value is an empty String if it is not available. final String product; /// An ordered list of 32 bit ABIs supported by this device. @@ -83,15 +107,23 @@ class AndroidDeviceInfo { final List supportedAbis; /// Comma-separated tags describing the build, like "unsigned,debug". + /// + /// The value is an empty String if it is not available. final String tags; /// The type of build, like "user" or "eng". + /// + /// The value is an empty String if it is not available. final String type; - /// `false` if the application is running in an emulator, `true` otherwise. + /// The value is `true` if the application is running on a physical device. + /// + /// The value is `false` when the application is running on a emulator, or the value is unavailable. final bool isPhysicalDevice; /// The Android hardware device ID that is unique between the device + user and app signing. + /// + /// The value is an empty String if it is not available. final String androidId; /// Describes what features are available on the current device. @@ -113,35 +145,41 @@ 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'] != null + ? 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'] ?? false, + androidId: map['androidId'] ?? '', + systemFeatures: _fromList(map['systemFeatures']), ); } /// Deserializes message as List static List _fromList(dynamic message) { - final List list = message; - return List.from(list); + if (message == null) { + return []; + } + assert(message is List); + final List list = List.from(message) + ..removeWhere((value) => value == null); + return list.cast(); } } @@ -152,47 +190,58 @@ 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. + /// + /// The value is an empty String if it is not available. final String codename; /// The internal value used by the underlying source control to represent this build. + /// + /// The value is an empty String if it is not available. final String incremental; - /// The developer preview revision of a prerelease SDK. - final int previewSdkInt; - /// The user-visible version string. + /// + /// The value is an empty String if it is not available. final String release; /// The user-visible SDK version of the framework. /// /// Possible values are defined in: https://developer.android.com/reference/android/os/Build.VERSION_CODES.html + /// + /// The value is -1 if it is unavailable. 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'] ?? -1, ); } } 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..20ec8362f66d 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,52 +8,71 @@ 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. + /// + /// The value is an empty String if it is not available. final String name; /// The name of the current operating system. + /// + /// The value is an empty String if it is not available. final String systemName; /// The current operating system version. + /// + /// The value is an empty String if it is not available. final String systemVersion; /// Device model. + /// + /// The value is an empty String if it is not available. final String model; /// Localized name of the device model. + /// + /// The value is an empty String if it is not available. final String localizedModel; /// Unique UUID value identifying the current device. + /// + /// The value is an empty String if it is not available. final String identifierForVendor; - /// `false` if the application is running in a simulator, `true` otherwise. + /// The value is `true` if the application is running on a physical device. + /// + /// The value is `false` when the application is running on a simulator, or the value is unavailable. final bool isPhysicalDevice; /// Operating system information derived from `sys/utsname.h`. + /// + /// The value is an empty String if it is not available. final IosUtsname utsname; /// 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'], - isPhysicalDevice: map['isPhysicalDevice'] == 'true', - utsname: - IosUtsname._fromMap(map['utsname']?.cast() ?? {}), + name: map['name'] ?? '', + systemName: map['systemName'] ?? '', + systemVersion: map['systemVersion'] ?? '', + model: map['model'] ?? '', + localizedModel: map['localizedModel'] ?? '', + identifierForVendor: map['identifierForVendor'] ?? '', + isPhysicalDevice: map['isPhysicalDevice'] != null + ? map['isPhysicalDevice'] == 'true' + : false, + utsname: IosUtsname._fromMap(map['utsname'] != null + ? map['utsname'].cast() + : {}), ); } } @@ -62,36 +81,46 @@ 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. + /// + /// The value is an empty String if it is not available. final String sysname; /// Network node name. + /// + /// The value is an empty String if it is not available. final String nodename; /// Release level. + /// + /// The value is an empty String if it is not available. final String release; /// Version level. + /// + /// The value is an empty String if it is not available. final String version; /// Hardware type (e.g. 'iPhone7,1' for iPhone 6 Plus). + /// + /// The value is an empty String if it is not available. final String machine; /// 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..3887aea3eff2 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 dependencies: flutter: sdk: flutter - meta: ^1.1.8 - plugin_platform_interface: ^1.0.2 + meta: ^1.3.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 + test: ^1.16.3 + pedantic: ^1.10.0 environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + sdk: ">=2.12.0-259.9.beta <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..03ff4b53cda9 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,282 @@ 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"); + }); + }); + + group( + "$MethodChannelDeviceInfo handles null value in the map returned from method channel", + () { + MethodChannelDeviceInfo methodChannelDeviceInfo; + + setUp(() async { + methodChannelDeviceInfo = MethodChannelDeviceInfo(); + + methodChannelDeviceInfo.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case 'getAndroidDeviceInfo': + return ({ + "version": null, + "board": null, + "bootloader": null, + "brand": null, + "device": null, + "display": null, + "fingerprint": null, + "hardware": null, + "host": null, + "id": null, + "manufacturer": null, + "model": null, + "product": null, + "supported32BitAbis": null, + "supported64BitAbis": null, + "supportedAbis": null, + "tags": null, + "type": null, + "isPhysicalDevice": null, + "androidId": null, + "systemFeatures": null, + }); + case 'getIosDeviceInfo': + return ({ + "name": null, + "systemName": null, + "systemVersion": null, + "model": null, + "localizedModel": null, + "identifierForVendor": null, + "isPhysicalDevice": null, + "utsname": null, + }); + default: + return null; + } + }); + }); + + test("androidInfo hanels null", () async { + final AndroidDeviceInfo result = + await methodChannelDeviceInfo.androidInfo(); + + expect(result.version.securityPatch, null); + expect(result.version.sdkInt, -1); + expect(result.version.release, ''); + expect(result.version.previewSdkInt, null); + expect(result.version.incremental, ''); + expect(result.version.codename, ''); + expect(result.board, ''); + expect(result.bootloader, ''); + expect(result.brand, ''); + expect(result.device, ''); + expect(result.display, ''); + expect(result.fingerprint, ''); + expect(result.hardware, ''); + expect(result.host, ''); + expect(result.id, ''); + expect(result.manufacturer, ''); + expect(result.model, ''); + expect(result.product, ''); + expect(result.supported32BitAbis, []); + expect(result.supported64BitAbis, []); + expect(result.supportedAbis, []); + expect(result.tags, ''); + expect(result.type, ''); + expect(result.isPhysicalDevice, false); + expect(result.androidId, ''); + expect(result.systemFeatures, []); + }); + + test("iosInfo handles null", () async { + final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); + expect(result.name, ''); + expect(result.systemName, ''); + expect(result.systemVersion, ''); + expect(result.model, ''); + expect(result.localizedModel, ''); + expect(result.identifierForVendor, ''); + expect(result.isPhysicalDevice, false); + expect(result.utsname.sysname, ''); + expect(result.utsname.nodename, ''); + expect(result.utsname.release, ''); + expect(result.utsname.version, ''); + expect(result.utsname.machine, ''); + }); + }); + + group("$MethodChannelDeviceInfo handles method channel returns null", () { + MethodChannelDeviceInfo methodChannelDeviceInfo; + + setUp(() async { + methodChannelDeviceInfo = MethodChannelDeviceInfo(); + + methodChannelDeviceInfo.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case 'getAndroidDeviceInfo': + return null; + case 'getIosDeviceInfo': + return null; + default: + return null; + } + }); + }); + + test("androidInfo handles null", () async { + final AndroidDeviceInfo result = + await methodChannelDeviceInfo.androidInfo(); + + expect(result.version.securityPatch, null); + expect(result.version.sdkInt, -1); + expect(result.version.release, ''); + expect(result.version.previewSdkInt, null); + expect(result.version.incremental, ''); + expect(result.version.codename, ''); + expect(result.board, ''); + expect(result.bootloader, ''); + expect(result.brand, ''); + expect(result.device, ''); + expect(result.display, ''); + expect(result.fingerprint, ''); + expect(result.hardware, ''); + expect(result.host, ''); + expect(result.id, ''); + expect(result.manufacturer, ''); + expect(result.model, ''); + expect(result.product, ''); + expect(result.supported32BitAbis, []); + expect(result.supported64BitAbis, []); + expect(result.supportedAbis, []); + expect(result.tags, ''); + expect(result.type, ''); + expect(result.isPhysicalDevice, false); + expect(result.androidId, ''); + expect(result.systemFeatures, []); + }); + + test("iosInfo handles null", () async { + final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); + expect(result.name, ''); + expect(result.systemName, ''); + expect(result.systemVersion, ''); + expect(result.model, ''); + expect(result.localizedModel, ''); + expect(result.identifierForVendor, ''); + expect(result.isPhysicalDevice, false); + expect(result.utsname.sysname, ''); + expect(result.utsname.nodename, ''); + expect(result.utsname.release, ''); + expect(result.utsname.version, ''); + expect(result.utsname.machine, ''); + }); + }); + + group("$MethodChannelDeviceInfo android handles null values in list", () { + MethodChannelDeviceInfo methodChannelDeviceInfo; + + setUp(() async { + methodChannelDeviceInfo = MethodChannelDeviceInfo(); + + methodChannelDeviceInfo.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case 'getAndroidDeviceInfo': + return ({ + "supported32BitAbis": ["x86", null], + "supported64BitAbis": ["x86_64", null], + "supportedAbis": ["x86_64", "x86", null], + "systemFeatures": [ + "android.hardware.sensor.proximity", + "android.software.adoptable_storage", + "android.hardware.sensor.accelerometer", + "android.hardware.faketouch", + "android.software.backup", + "android.hardware.touchscreen", + null + ], + }); + default: + return null; + } + }); + }); + + test("androidInfo hanels null in list", () async { + final AndroidDeviceInfo result = + await methodChannelDeviceInfo.androidInfo(); + expect(result.supported32BitAbis, ['x86']); + expect(result.supported64BitAbis, ['x86_64']); + expect(result.supportedAbis, ['x86_64', 'x86']); + expect(result.systemFeatures, [ + "android.hardware.sensor.proximity", + "android.software.adoptable_storage", + "android.hardware.sensor.accelerometer", + "android.hardware.faketouch", + "android.software.backup", + "android.hardware.touchscreen" + ]); }); }); } diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index a9d61f1178c2..454736454cdf 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.1.0 + +* Update SDK requirement for null-safety compatibility. + +## 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/example/ios/Flutter/AppFrameworkInfo.plist b/packages/espresso/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6b4c0f78a785..000000000000 --- a/packages/espresso/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 8.0 - - diff --git a/packages/espresso/example/ios/Flutter/Debug.xcconfig b/packages/espresso/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/espresso/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/espresso/example/ios/Flutter/Release.xcconfig b/packages/espresso/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/espresso/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/espresso/example/ios/Runner.xcodeproj/project.pbxproj b/packages/espresso/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 2209e01dfcd6..000000000000 --- a/packages/espresso/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,584 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* 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, ); }; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 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, ); }; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - B4A70C1E3465B7A2E7ECD8F8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE5F32230E1B4F4C17EDB557 /* Pods_Runner.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 02691CEFCB33C0B1CABE7A23 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 09442C04D3DC0049E7725D93 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 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 = ""; }; - 3EF237100A0BFC444DE6BC97 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; 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; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AE5F32230E1B4F4C17EDB557 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - B4A70C1E3465B7A2E7ECD8F8 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 301432828879F7BDE0943C41 /* Frameworks */ = { - isa = PBXGroup; - children = ( - AE5F32230E1B4F4C17EDB557 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - E9E5CC94EC52B9D261A44A5E /* Pods */, - 301432828879F7BDE0943C41 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - ); - name = "Supporting Files"; - sourceTree = ""; - }; - E9E5CC94EC52B9D261A44A5E /* Pods */ = { - isa = PBXGroup; - children = ( - 02691CEFCB33C0B1CABE7A23 /* Pods-Runner.debug.xcconfig */, - 3EF237100A0BFC444DE6BC97 /* Pods-Runner.release.xcconfig */, - 09442C04D3DC0049E7725D93 /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 5D7E711796DC6F61E7F1A6AE /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - DC7821945A6EDE472DDF686F /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1020; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 5D7E711796DC6F61E7F1A6AE /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - DC7821945A6EDE472DDF686F /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.espressoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.espressoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.espressoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/espresso/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/espresso/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index a28140cfdb3f..000000000000 --- a/packages/espresso/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fab2d9d..000000000000 --- a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2fd467..000000000000 --- a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eacad3b..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eacad3b..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eacad3b..000000000000 Binary files a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b70f1..000000000000 --- a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/espresso/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/espresso/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7c939..000000000000 --- a/packages/espresso/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/espresso/example/ios/Runner/Base.lproj/Main.storyboard b/packages/espresso/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/espresso/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/espresso/example/ios/Runner/Info.plist b/packages/espresso/example/ios/Runner/Info.plist deleted file mode 100644 index 96cc992ec974..000000000000 --- a/packages/espresso/example/ios/Runner/Info.plist +++ /dev/null @@ -1,45 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - espresso_example - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/espresso/example/ios/Runner/Runner-Bridging-Header.h b/packages/espresso/example/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 7335fdf9000c..000000000000 --- a/packages/espresso/example/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/packages/espresso/example/lib/main.dart b/packages/espresso/example/lib/main.dart index c74423f507e8..958d26a0c149 100644 --- a/packages/espresso/example/lib/main.dart +++ b/packages/espresso/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); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect diff --git a/packages/espresso/example/pubspec.yaml b/packages/espresso/example/pubspec.yaml index d2d73da3c0ae..6e824acb4080 100644 --- a/packages/espresso/example/pubspec.yaml +++ b/packages/espresso/example/pubspec.yaml @@ -3,64 +3,27 @@ description: Demonstrates how to use the espresso plugin. publish_to: 'none' environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 - dev_dependencies: flutter_test: sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 espresso: + # When depending on this package from a real application you should use: + # espresso: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, 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 - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/espresso/ios/.gitignore b/packages/espresso/ios/.gitignore deleted file mode 100644 index aa479fd3ce8a..000000000000 --- a/packages/espresso/ios/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/espresso/ios/Assets/.gitkeep b/packages/espresso/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/espresso/ios/Classes/EspressoPlugin.h b/packages/espresso/ios/Classes/EspressoPlugin.h deleted file mode 100644 index 5f9761591f72..000000000000 --- a/packages/espresso/ios/Classes/EspressoPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface EspressoPlugin : NSObject -@end diff --git a/packages/espresso/ios/Classes/EspressoPlugin.m b/packages/espresso/ios/Classes/EspressoPlugin.m deleted file mode 100644 index cb4ef8072cae..000000000000 --- a/packages/espresso/ios/Classes/EspressoPlugin.m +++ /dev/null @@ -1,15 +0,0 @@ -#import "EspressoPlugin.h" - -@implementation EspressoPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"espresso" - binaryMessenger:[registrar messenger]]; - EspressoPlugin* instance = [[EspressoPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - result(FlutterMethodNotImplemented); -} -@end diff --git a/packages/espresso/ios/espresso.podspec b/packages/espresso/ios/espresso.podspec deleted file mode 100644 index c9b2d106fd92..000000000000 --- a/packages/espresso/ios/espresso.podspec +++ /dev/null @@ -1,25 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint espresso.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'espresso' - s.version = '0.0.1' - s.summary = 'Flutter Espresso' - s.description = <<-DESC -Provides bindings for Espresso tests of Flutter apps. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/espresso' } - s.documentation_url = 'https://pub.dev/packages/espresso' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml index fe284260b73c..90b485fdc48d 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.1.0 homepage: https://github.com/flutter/plugins/espresso environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: @@ -14,7 +14,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 # The following section is specific to Flutter. flutter: @@ -23,5 +23,3 @@ flutter: android: package: com.example.espresso pluginClass: EspressoPlugin - ios: - pluginClass: EspressoPlugin diff --git a/packages/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md new file mode 100644 index 000000000000..cea752e51558 --- /dev/null +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -0,0 +1,19 @@ +## 0.8.1 + +Endorse the web implementation. + +## 0.8.0 + +Migrate to null safety. + +## 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/LICENSE b/packages/file_selector/file_selector/LICENSE new file mode 100644 index 000000000000..2c91f1438173 --- /dev/null +++ b/packages/file_selector/file_selector/LICENSE @@ -0,0 +1,25 @@ +Copyright 2020 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/file_selector/file_selector/README.md b/packages/file_selector/file_selector/README.md new file mode 100644 index 000000000000..22ae7073ca2d --- /dev/null +++ b/packages/file_selector/file_selector/README.md @@ -0,0 +1,36 @@ +# file_selector + +[![pub package](https://img.shields.io/pub/v/file_selector.svg)](https://pub.dartlang.org/packages/file_selector) + +A Flutter plugin that manages files and interactions with file dialogs. + +## Usage +To use this plugin, add `file_selector` as a [dependency in your pubspec.yaml file](https://flutter.dev/platform-plugins/). + +### Examples +Here are small examples that show you how to use the API. +Please also take a look at our [example][example] app. + +#### Open a single file +``` dart +final typeGroup = XTypeGroup(label: 'images', extensions: ['jpg', 'png']); +final file = await openFile(acceptedTypeGroups: [typeGroup]); +``` + +#### Open multiple files at once +``` dart +final typeGroup = XTypeGroup(label: 'images', extensions: ['jpg', 'png']); +final files = await openFiles(acceptedTypeGroups: [typeGroup]); +``` + +#### Saving a file +```dart +final path = await getSavePath(); +final name = "hello_file_selector.txt"; +final data = Uint8List.fromList("Hello World!".codeUnits); +final mimeType = "text/plain"; +final file = XFile.fromData(data, name: name, mimeType: mimeType); +await file.saveTo(path); +``` + +[example]:./example diff --git a/packages/file_selector/file_selector/example/.gitignore b/packages/file_selector/file_selector/example/.gitignore new file mode 100644 index 000000000000..7abd0753cfc3 --- /dev/null +++ b/packages/file_selector/file_selector/example/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Currently only web supported +android/ +ios/ + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/file_selector/file_selector/example/.metadata b/packages/file_selector/file_selector/example/.metadata new file mode 100644 index 000000000000..897381f2373f --- /dev/null +++ b/packages/file_selector/file_selector/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 7736f3bc90270dcb0480db2ccffbf1d13c28db85 + channel: dev + +project_type: app diff --git a/packages/file_selector/file_selector/example/README.md b/packages/file_selector/file_selector/example/README.md new file mode 100644 index 000000000000..93260dc716b2 --- /dev/null +++ b/packages/file_selector/file_selector/example/README.md @@ -0,0 +1,8 @@ +# file_selector_example + +Demonstrates how to use the file_selector plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). 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 new file mode 100644 index 000000000000..cf4cde9fa9a8 --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/get_directory_page.dart @@ -0,0 +1,71 @@ +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; + +/// Screen that shows an example of getDirectoryPath +class GetDirectoryPage extends StatelessWidget { + void _getDirectoryPath(BuildContext context) async { + final String confirmButtonText = 'Choose'; + final String? directoryPath = await getDirectoryPath( + confirmButtonText: confirmButtonText, + ); + if (directoryPath == null) { + // Operation was canceled by the user. + return; + } + await showDialog( + context: context, + builder: (context) => TextDisplay(directoryPath), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Open a text file"), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: Text('Press to ask user to choose a directory'), + onPressed: () => _getDirectoryPath(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog +class TextDisplay extends StatelessWidget { + /// Directory path + final String directoryPath; + + /// Default Constructor + TextDisplay(this.directoryPath); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('Selected Directory'), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(directoryPath), + ), + ), + actions: [ + 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 new file mode 100644 index 000000000000..1cb7ef261e88 --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/home_page.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; + +/// Home Page of the application +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'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: style, + child: Text('Open a text file'), + onPressed: () => Navigator.pushNamed(context, '/open/text'), + ), + SizedBox(height: 10), + ElevatedButton( + style: style, + child: Text('Open an image'), + onPressed: () => Navigator.pushNamed(context, '/open/image'), + ), + SizedBox(height: 10), + ElevatedButton( + style: style, + child: Text('Open multiple images'), + onPressed: () => Navigator.pushNamed(context, '/open/images'), + ), + SizedBox(height: 10), + ElevatedButton( + style: style, + child: Text('Save a file'), + onPressed: () => Navigator.pushNamed(context, '/save/text'), + ), + SizedBox(height: 10), + 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/main.dart b/packages/file_selector/file_selector/example/lib/main.dart new file mode 100644 index 000000000000..bb34918e36a3 --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/main.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:example/home_page.dart'; +import 'package:example/get_directory_page.dart'; +import 'package:example/open_text_page.dart'; +import 'package:example/open_image_page.dart'; +import 'package:example/open_multiple_images_page.dart'; +import 'package:example/save_text_page.dart'; + +void main() { + runApp(MyApp()); +} + +/// MyApp is the Main Application +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'File Selector Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: HomePage(), + routes: { + '/open/image': (context) => OpenImagePage(), + '/open/images': (context) => OpenMultipleImagesPage(), + '/open/text': (context) => OpenTextPage(), + '/save/text': (context) => SaveTextPage(), + '/directory': (context) => GetDirectoryPage(), + }, + ); + } +} 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 new file mode 100644 index 000000000000..986bfe712893 --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/open_image_page.dart @@ -0,0 +1,81 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; + +/// Screen that shows an example of openFiles +class OpenImagePage extends StatelessWidget { + void _openImageFile(BuildContext context) async { + final XTypeGroup typeGroup = XTypeGroup( + label: 'images', + extensions: ['jpg', 'png'], + ); + final List files = await openFiles(acceptedTypeGroups: [typeGroup]); + if (files.isEmpty) { + // Operation was canceled by the user. + return; + } + final XFile file = files[0]; + final String fileName = file.name; + final String filePath = file.path; + + await showDialog( + context: context, + builder: (context) => ImageDisplay(fileName, filePath), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Open an image"), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: Text('Press to open an image file(png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog +class ImageDisplay extends StatelessWidget { + /// Image's name + final String fileName; + + /// Image's path + final String filePath; + + /// Default Constructor + ImageDisplay(this.fileName, this.filePath); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: kIsWeb ? Image.network(filePath) : Image.file(File(filePath)), + actions: [ + 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 new file mode 100644 index 000000000000..c6f73f5aed12 --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart @@ -0,0 +1,92 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; + +/// Screen that shows an example of openFiles +class OpenMultipleImagesPage extends StatelessWidget { + void _openImageFile(BuildContext context) async { + final XTypeGroup jpgsTypeGroup = XTypeGroup( + label: 'JPEGs', + extensions: ['jpg', 'jpeg'], + ); + final XTypeGroup pngTypeGroup = XTypeGroup( + label: 'PNGs', + extensions: ['png'], + ); + final List files = await openFiles(acceptedTypeGroups: [ + jpgsTypeGroup, + pngTypeGroup, + ]); + if (files.isEmpty) { + // Operation was canceled by the user. + return; + } + await showDialog( + context: context, + builder: (context) => MultipleImagesDisplay(files), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Open multiple images"), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: Text('Press to open multiple images (png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog +class MultipleImagesDisplay extends StatelessWidget { + /// The files containing the images + final List files; + + /// Default Constructor + MultipleImagesDisplay(this.files); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('Gallery'), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: Center( + child: Row( + children: [ + ...files.map( + (file) => Flexible( + child: kIsWeb + ? Image.network(file.path) + : Image.file(File(file.path))), + ) + ], + ), + ), + actions: [ + 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 new file mode 100644 index 000000000000..74d79dd72b19 --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/open_text_page.dart @@ -0,0 +1,78 @@ +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; + +/// Screen that shows an example of openFile +class OpenTextPage extends StatelessWidget { + void _openTextFile(BuildContext context) async { + final XTypeGroup typeGroup = XTypeGroup( + label: 'text', + extensions: ['txt', 'json'], + ); + final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) { + // Operation was canceled by the user. + return; + } + final String fileName = file.name; + final String fileContent = await file.readAsString(); + + await showDialog( + context: context, + builder: (context) => TextDisplay(fileName, fileContent), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Open a text file"), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: Text('Press to open a text file (json, txt)'), + onPressed: () => _openTextFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog +class TextDisplay extends StatelessWidget { + /// File's name + final String fileName; + + /// File to display + final String fileContent; + + /// Default Constructor + TextDisplay(this.fileName, this.fileContent); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(fileContent), + ), + ), + actions: [ + 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 new file mode 100644 index 000000000000..82046c35128a --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/save_text_page.dart @@ -0,0 +1,71 @@ +import 'dart:typed_data'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; + +/// Page for showing an example of saving with file_selector +class SaveTextPage extends StatelessWidget { + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _contentController = TextEditingController(); + + void _saveFile() async { + String? path = await getSavePath(); + if (path == null) { + // Operation was canceled by the user. + return; + } + final String text = _contentController.text; + final String fileName = _nameController.text; + final Uint8List fileData = Uint8List.fromList(text.codeUnits); + final String fileMimeType = 'text/plain'; + final XFile textFile = + XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); + await textFile.saveTo(path); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Save text into a file"), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 300, + child: TextField( + minLines: 1, + maxLines: 12, + controller: _nameController, + decoration: InputDecoration( + hintText: '(Optional) Suggest File Name', + ), + ), + ), + Container( + width: 300, + child: TextField( + minLines: 1, + maxLines: 12, + controller: _contentController, + decoration: InputDecoration( + hintText: 'Enter File Contents', + ), + ), + ), + SizedBox(height: 10), + 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/example/pubspec.yaml b/packages/file_selector/file_selector/example/pubspec.yaml new file mode 100644 index 000000000000..580237cad5ac --- /dev/null +++ b/packages/file_selector/file_selector/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: example +description: A new Flutter project. + +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +version: 1.0.0+1 + +environment: + sdk: ">=2.12.0-259.9.beta <3.0.0" + +dependencies: + flutter: + sdk: flutter + + file_selector: + # When depending on this package from a real application you should use: + # file_selector: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/file_selector/file_selector/example/web/favicon.png b/packages/file_selector/file_selector/example/web/favicon.png new file mode 100644 index 000000000000..8aaa46ac1ae2 Binary files /dev/null and b/packages/file_selector/file_selector/example/web/favicon.png differ diff --git a/packages/file_selector/file_selector/example/web/icons/Icon-192.png b/packages/file_selector/file_selector/example/web/icons/Icon-192.png new file mode 100644 index 000000000000..b749bfef0747 Binary files /dev/null and b/packages/file_selector/file_selector/example/web/icons/Icon-192.png differ diff --git a/packages/file_selector/file_selector/example/web/icons/Icon-512.png b/packages/file_selector/file_selector/example/web/icons/Icon-512.png new file mode 100644 index 000000000000..88cfd48dff11 Binary files /dev/null and b/packages/file_selector/file_selector/example/web/icons/Icon-512.png differ diff --git a/packages/file_selector/file_selector/example/web/index.html b/packages/file_selector/file_selector/example/web/index.html new file mode 100644 index 000000000000..9b7a438f823a --- /dev/null +++ b/packages/file_selector/file_selector/example/web/index.html @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + example + + + + + + + + diff --git a/packages/file_selector/file_selector/example/web/manifest.json b/packages/file_selector/file_selector/example/web/manifest.json new file mode 100644 index 000000000000..8c012917dab7 --- /dev/null +++ b/packages/file_selector/file_selector/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/packages/file_selector/file_selector/lib/file_selector.dart b/packages/file_selector/file_selector/lib/file_selector.dart new file mode 100644 index 000000000000..cdb2bf9c726d --- /dev/null +++ b/packages/file_selector/file_selector/lib/file_selector.dart @@ -0,0 +1,57 @@ +// 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. + +import 'dart:async'; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; + +export 'package:file_selector_platform_interface/file_selector_platform_interface.dart' + show XFile, XTypeGroup; + +/// Open file dialog for loading files and return a file path +Future openFile({ + List acceptedTypeGroups = const [], + String? initialDirectory, + String? confirmButtonText, +}) { + return FileSelectorPlatform.instance.openFile( + acceptedTypeGroups: acceptedTypeGroups, + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText); +} + +/// Open file dialog for loading files and return a list of file paths +Future> openFiles({ + List acceptedTypeGroups = const [], + String? initialDirectory, + String? confirmButtonText, +}) { + return FileSelectorPlatform.instance.openFiles( + acceptedTypeGroups: acceptedTypeGroups, + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText); +} + +/// Saves File to user's file system +Future getSavePath({ + List acceptedTypeGroups = const [], + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, +}) async { + return FileSelectorPlatform.instance.getSavePath( + acceptedTypeGroups: acceptedTypeGroups, + initialDirectory: initialDirectory, + suggestedName: suggestedName, + confirmButtonText: confirmButtonText); +} + +/// Gets a directory path from a user's file system +Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, +}) async { + return FileSelectorPlatform.instance.getDirectoryPath( + initialDirectory: initialDirectory, confirmButtonText: confirmButtonText); +} diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml new file mode 100644 index 000000000000..3d03de09e9f2 --- /dev/null +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -0,0 +1,27 @@ +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.8.1 + +flutter: + plugin: + platforms: + web: + default_package: file_selector_web + +dependencies: + flutter: + sdk: flutter + file_selector_platform_interface: ^2.0.0 + file_selector_web: ^0.8.1 + +dev_dependencies: + flutter_test: + sdk: flutter + test: ^1.16.3 + plugin_platform_interface: ">=1.0.0 <3.0.0" + pedantic: ^1.10.0 + +environment: + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/file_selector/file_selector/test/file_selector_test.dart b/packages/file_selector/file_selector/test/file_selector_test.dart new file mode 100644 index 000000000000..b16f234bb3b8 --- /dev/null +++ b/packages/file_selector/file_selector/test/file_selector_test.dart @@ -0,0 +1,337 @@ +// 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. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:test/fake.dart'; + +void main() { + late FakeFileSelector fakePlatformImplementation; + final initialDirectory = '/home/flutteruser'; + final confirmButtonText = 'Use this profile picture'; + final suggestedName = 'suggested_name'; + final acceptedTypeGroups = [ + XTypeGroup(label: 'documents', mimeTypes: [ + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessing', + ]), + XTypeGroup(label: 'images', extensions: [ + 'jpg', + 'png', + ]), + ]; + + setUp(() { + fakePlatformImplementation = FakeFileSelector(); + FileSelectorPlatform.instance = fakePlatformImplementation; + }); + + group('openFile', () { + final expectedFile = XFile('path'); + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups) + ..setFileResponse([expectedFile]); + + final file = await openFile( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + ); + + expect(file, expectedFile); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setFileResponse([expectedFile]); + + final file = await openFile(); + + expect(file, expectedFile); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setFileResponse([expectedFile]); + + final file = await openFile(initialDirectory: initialDirectory); + expect(file, expectedFile); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setFileResponse([expectedFile]); + + final file = await openFile(confirmButtonText: confirmButtonText); + expect(file, expectedFile); + }); + + test('sets the accepted type groups', () async { + fakePlatformImplementation + ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) + ..setFileResponse([expectedFile]); + + final file = await openFile(acceptedTypeGroups: acceptedTypeGroups); + expect(file, expectedFile); + }); + }); + + group('openFiles', () { + final expectedFiles = [XFile('path')]; + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups) + ..setFileResponse(expectedFiles); + + final file = await openFiles( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + ); + + expect(file, expectedFiles); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setFileResponse(expectedFiles); + + final files = await openFiles(); + + expect(files, expectedFiles); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setFileResponse(expectedFiles); + + final files = await openFiles(initialDirectory: initialDirectory); + expect(files, expectedFiles); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setFileResponse(expectedFiles); + + final files = await openFiles(confirmButtonText: confirmButtonText); + expect(files, expectedFiles); + }); + + test('sets the accepted type groups', () async { + fakePlatformImplementation + ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) + ..setFileResponse(expectedFiles); + + final files = await openFiles(acceptedTypeGroups: acceptedTypeGroups); + expect(files, expectedFiles); + }); + }); + + group('getSavePath', () { + final expectedSavePath = '/example/path'; + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + suggestedName: suggestedName) + ..setPathResponse(expectedSavePath); + + final savePath = await getSavePath( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + suggestedName: suggestedName, + ); + + expect(savePath, expectedSavePath); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setPathResponse(expectedSavePath); + + final savePath = await getSavePath(); + expect(savePath, expectedSavePath); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setPathResponse(expectedSavePath); + + final savePath = await getSavePath(initialDirectory: initialDirectory); + expect(savePath, expectedSavePath); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setPathResponse(expectedSavePath); + + final savePath = await getSavePath(confirmButtonText: confirmButtonText); + expect(savePath, expectedSavePath); + }); + + test('sets the accepted type groups', () async { + fakePlatformImplementation + ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) + ..setPathResponse(expectedSavePath); + + final savePath = + await getSavePath(acceptedTypeGroups: acceptedTypeGroups); + expect(savePath, expectedSavePath); + }); + + test('sets the suggested name', () async { + fakePlatformImplementation + ..setExpectations(suggestedName: suggestedName) + ..setPathResponse(expectedSavePath); + + final savePath = await getSavePath(suggestedName: suggestedName); + expect(savePath, expectedSavePath); + }); + }); + + group('getDirectoryPath', () { + final expectedDirectoryPath = '/example/path'; + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText) + ..setPathResponse(expectedDirectoryPath); + + final directoryPath = await getDirectoryPath( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + ); + + expect(directoryPath, expectedDirectoryPath); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setPathResponse(expectedDirectoryPath); + + final directoryPath = await getDirectoryPath(); + expect(directoryPath, expectedDirectoryPath); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setPathResponse(expectedDirectoryPath); + + final directoryPath = + await getDirectoryPath(initialDirectory: initialDirectory); + expect(directoryPath, expectedDirectoryPath); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setPathResponse(expectedDirectoryPath); + + final directoryPath = + await getDirectoryPath(confirmButtonText: confirmButtonText); + expect(directoryPath, expectedDirectoryPath); + }); + }); +} + +class FakeFileSelector extends Fake + with MockPlatformInterfaceMixin + implements FileSelectorPlatform { + // Expectations. + List? acceptedTypeGroups = const []; + String? initialDirectory; + String? confirmButtonText; + String? suggestedName; + // Return values. + List? files; + String? path; + + void setExpectations({ + List acceptedTypeGroups = const [], + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) { + this.acceptedTypeGroups = acceptedTypeGroups; + this.initialDirectory = initialDirectory; + this.suggestedName = suggestedName; + this.confirmButtonText = confirmButtonText; + } + + void setFileResponse(List files) { + this.files = files; + } + + void setPathResponse(String path) { + this.path = path; + } + + @override + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + expect(acceptedTypeGroups, this.acceptedTypeGroups); + expect(initialDirectory, this.initialDirectory); + expect(suggestedName, this.suggestedName); + return files?[0]; + } + + @override + Future> openFiles({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + expect(acceptedTypeGroups, this.acceptedTypeGroups); + expect(initialDirectory, this.initialDirectory); + expect(suggestedName, this.suggestedName); + return files!; + } + + @override + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) async { + expect(acceptedTypeGroups, this.acceptedTypeGroups); + expect(initialDirectory, this.initialDirectory); + expect(suggestedName, this.suggestedName); + expect(confirmButtonText, this.confirmButtonText); + return path; + } + + @override + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) async { + expect(initialDirectory, this.initialDirectory); + expect(confirmButtonText, this.confirmButtonText); + return path; + } +} diff --git a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md index 3663064a069f..8fcc3e06ef49 100644 --- a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md +++ b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md @@ -1,3 +1,23 @@ +## 2.0.1 + +* Replace extensions with leading dots. + +## 2.0.0 + +* Migration to null-safety + +## 1.0.3+1 + +* Bump the [cross_file](https://pub.dev/packages/cross_file) package version. + +## 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..e14239f51690 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'; @@ -18,57 +19,57 @@ class MethodChannelFileSelector extends FileSelectorPlatform { /// Load a file from user's computer and return it as an XFile @override - Future openFile({ - List acceptedTypeGroups, - String initialDirectory, - String confirmButtonText, + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, }) async { - final List path = await _channel.invokeListMethod( + final List? path = await _channel.invokeListMethod( 'openFile', { 'acceptedTypeGroups': - acceptedTypeGroups?.map((group) => group.toJSON())?.toList(), + acceptedTypeGroups?.map((group) => group.toJSON()).toList(), 'initialDirectory': initialDirectory, 'confirmButtonText': confirmButtonText, 'multiple': false, }, ); - return path == null ? null : XFile(path?.first); + return path == null ? null : XFile(path.first); } /// Load multiple files from user's computer and return it as an XFile @override Future> openFiles({ - List acceptedTypeGroups, - String initialDirectory, - String confirmButtonText, + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, }) async { - final List pathList = await _channel.invokeListMethod( + final List? pathList = await _channel.invokeListMethod( 'openFile', { 'acceptedTypeGroups': - acceptedTypeGroups?.map((group) => group.toJSON())?.toList(), + acceptedTypeGroups?.map((group) => group.toJSON()).toList(), 'initialDirectory': initialDirectory, 'confirmButtonText': confirmButtonText, 'multiple': true, }, ); - return pathList?.map((path) => XFile(path))?.toList() ?? []; + return pathList?.map((path) => XFile(path)).toList() ?? []; } /// Gets the path from a save dialog @override - Future getSavePath({ - List acceptedTypeGroups, - String initialDirectory, - String suggestedName, - String confirmButtonText, + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, }) async { return _channel.invokeMethod( 'getSavePath', { 'acceptedTypeGroups': - acceptedTypeGroups?.map((group) => group.toJSON())?.toList(), + acceptedTypeGroups?.map((group) => group.toJSON()).toList(), 'initialDirectory': initialDirectory, 'suggestedName': suggestedName, 'confirmButtonText': confirmButtonText, @@ -78,9 +79,9 @@ class MethodChannelFileSelector extends FileSelectorPlatform { /// Gets a directory path from a dialog @override - Future getDirectoryPath({ - String initialDirectory, - String confirmButtonText, + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, }) async { return _channel.invokeMethod( 'getDirectoryPath', 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..0be02c2185dd 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'; @@ -37,37 +38,40 @@ abstract class FileSelectorPlatform extends PlatformInterface { } /// Open file dialog for loading files and return a file path - Future openFile({ - List acceptedTypeGroups, - String initialDirectory, - String confirmButtonText, + /// Returns `null` if user cancels the operation. + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, }) { throw UnimplementedError('openFile() has not been implemented.'); } /// Open file dialog for loading files and return a list of file paths Future> openFiles({ - List acceptedTypeGroups, - String initialDirectory, - String confirmButtonText, + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, }) { throw UnimplementedError('openFiles() has not been implemented.'); } /// Open file dialog for saving files and return a file path at which to save - Future getSavePath({ - List acceptedTypeGroups, - String initialDirectory, - String suggestedName, - String confirmButtonText, + /// Returns `null` if user cancels the operation. + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, }) { throw UnimplementedError('getSavePath() has not been implemented.'); } /// Open file dialog for loading directories and return a directory path - Future getDirectoryPath({ - String initialDirectory, - String confirmButtonText, + /// Returns `null` if user cancels the operation. + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, }) { throw UnimplementedError('getDirectoryPath() has not been implemented.'); } 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/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/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/lib/src/types/x_type_group/x_type_group.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart index fb591f2b248a..7b3cb12be9f1 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart @@ -10,26 +10,26 @@ class XTypeGroup { /// allowed. XTypeGroup({ this.label, - this.extensions, + List? extensions, this.mimeTypes, this.macUTIs, this.webWildCards, - }); + }) : this.extensions = _removeLeadingDots(extensions); /// The 'name' or reference to this group of types - final String label; + final String? label; /// The extensions for this group - final List extensions; + final List? extensions; /// The MIME types for this group - final List mimeTypes; + final List? mimeTypes; /// The UTIs for this group - final List macUTIs; + final List? macUTIs; /// The web wild cards for this group (ex: image/*, video/*) - final List webWildCards; + final List? webWildCards; /// Converts this object into a JSON formatted object Map toJSON() { @@ -41,4 +41,7 @@ class XTypeGroup { 'webWildCards': webWildCards, }; } + + static List? _removeLeadingDots(List? exts) => + exts?.map((ext) => ext.startsWith('.') ? ext.substring(1) : ext).toList(); } diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart b/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart index 9e40e562bc9a..5330c5cf6dcd 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart @@ -1,7 +1,7 @@ import 'dart:html'; /// Create anchor element with download attribute -AnchorElement createAnchorElement(String href, String suggestedName) { +AnchorElement createAnchorElement(String href, String? suggestedName) { final element = AnchorElement(href: href); if (suggestedName == null) { @@ -27,7 +27,7 @@ Element ensureInitialized(String id) { if (target == null) { final Element targetElement = Element.tag('flt-x-file')..id = id; - querySelector('body').children.add(targetElement); + querySelector('body')!.children.add(targetElement); target = targetElement; } return target; diff --git a/packages/file_selector/file_selector_platform_interface/pubspec.yaml b/packages/file_selector/file_selector_platform_interface/pubspec.yaml index ca5c2bd189ae..980730eb2676 100644 --- a/packages/file_selector/file_selector_platform_interface/pubspec.yaml +++ b/packages/file_selector/file_selector_platform_interface/pubspec.yaml @@ -3,22 +3,22 @@ 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: 2.0.1 dependencies: flutter: sdk: flutter - meta: ^1.0.5 - http: ^0.12.0+1 - plugin_platform_interface: ^1.0.1 + meta: ^1.3.0 + http: ^0.13.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" + cross_file: ^0.3.0 dev_dependencies: - test: ^1.15.0 + test: ^1.16.3 flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart index 6809cee66963..56f6ae91bf28 100644 --- a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart @@ -2,9 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:mockito/mockito.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_platform_interface/src/method_channel/method_channel_file_selector.dart'; @@ -16,28 +14,10 @@ void main() { isInstanceOf()); }); - test('Cannot be implemented with `implements`', () { - expect(() { - FileSelectorPlatform.instance = ImplementsFileSelectorPlatform(); - }, throwsA(isInstanceOf())); - }); - - test('Can be mocked with `implements`', () { - final FileSelectorPlatformMock mock = FileSelectorPlatformMock(); - FileSelectorPlatform.instance = mock; - }); - test('Can be extended', () { FileSelectorPlatform.instance = ExtendsFileSelectorPlatform(); }); }); } -class FileSelectorPlatformMock extends Mock - with MockPlatformInterfaceMixin - implements FileSelectorPlatform {} - -class ImplementsFileSelectorPlatform extends Mock - implements FileSelectorPlatform {} - class ExtendsFileSelectorPlatform extends FileSelectorPlatform {} diff --git a/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart b/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart index 99f9fe0f0e3b..c863ad361112 100644 --- a/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart @@ -29,14 +29,14 @@ void main() { test('passes the accepted type groups correctly', () async { final group = XTypeGroup( label: 'text', - extensions: ['.txt'], + extensions: ['txt'], mimeTypes: ['text/plain'], macUTIs: ['public.text'], ); final groupTwo = XTypeGroup( label: 'image', - extensions: ['.jpg'], + extensions: ['jpg'], mimeTypes: ['image/jpg'], macUTIs: ['public.image'], webWildCards: ['image/*']); @@ -90,14 +90,14 @@ void main() { test('passes the accepted type groups correctly', () async { final group = XTypeGroup( label: 'text', - extensions: ['.txt'], + extensions: ['txt'], mimeTypes: ['text/plain'], macUTIs: ['public.text'], ); final groupTwo = XTypeGroup( label: 'image', - extensions: ['.jpg'], + extensions: ['jpg'], mimeTypes: ['image/jpg'], macUTIs: ['public.image'], webWildCards: ['image/*']); @@ -152,14 +152,14 @@ void main() { test('passes the accepted type groups correctly', () async { final group = XTypeGroup( label: 'text', - extensions: ['.txt'], + extensions: ['txt'], mimeTypes: ['text/plain'], macUTIs: ['public.text'], ); final groupTwo = XTypeGroup( label: 'image', - extensions: ['.jpg'], + extensions: ['jpg'], mimeTypes: ['image/jpg'], macUTIs: ['public.image'], webWildCards: ['image/*']); diff --git a/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart b/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart index bde89f46405d..21bbac5d0f90 100644 --- a/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart +++ b/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart @@ -9,7 +9,7 @@ void main() { group('XTypeGroup', () { test('toJSON() creates correct map', () { final label = 'test group'; - final extensions = ['.txt', '.jpg']; + final extensions = ['txt', 'jpg']; final mimeTypes = ['text/plain']; final macUTIs = ['public.plain-text']; final webWildCards = ['image/*']; @@ -41,5 +41,12 @@ void main() { expect(jsonMap['macUTIs'], null); expect(jsonMap['webWildCards'], null); }); + + test('Leading dots are removed from extensions', () { + final extensions = ['.txt', '.jpg']; + final group = XTypeGroup(extensions: extensions); + + expect(group.extensions, ['txt', 'jpg']); + }); }); } diff --git a/packages/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md new file mode 100644 index 000000000000..3eb7c3b94494 --- /dev/null +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -0,0 +1,16 @@ +# 0.8.1 + +- Return a non-null value from `getSavePath` for consistency with + API expectations that null indicates canceling. + +# 0.8.0 + +- Migrated to null-safety + +# 0.7.0+1 + +- Add dummy `ios` dir, so flutter sdk can be lower than 1.20 + +# 0.7.0 + +- Initial open-source release. diff --git a/packages/file_selector/file_selector_web/LICENSE b/packages/file_selector/file_selector_web/LICENSE new file mode 100644 index 000000000000..2c91f1438173 --- /dev/null +++ b/packages/file_selector/file_selector_web/LICENSE @@ -0,0 +1,25 @@ +Copyright 2020 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/file_selector/file_selector_web/README.md b/packages/file_selector/file_selector_web/README.md new file mode 100644 index 000000000000..24d48f48586f --- /dev/null +++ b/packages/file_selector/file_selector_web/README.md @@ -0,0 +1,30 @@ +# file_selector_web + +The web implementation of [`file_selector`][1]. + +## Usage + +### Import the package +To use this plugin in your Flutter Web app, simply add it as a dependency in +your pubspec alongside the base `file_selector` plugin. + +_(This is only temporary: in the future we hope to make this package an +"endorsed" implementation of `file_selector`, so that it is automatically +included in your Flutter Web app when you depend on `package:file_selector`.)_ + +This is what the above means to your `pubspec.yaml`: + +```yaml +... +dependencies: + ... + file_selector: ^0.7.0 + file_selector_web: ^0.7.0 + ... +``` + +### Use the plugin +Once you have the `file_selector_web` dependency in your pubspec, you should +be able to use `package:file_selector` as normal. + +[1]: https://pub.dev/packages/file_selector diff --git a/packages/file_selector/file_selector_web/example/README.md b/packages/file_selector/file_selector_web/example/README.md new file mode 100644 index 000000000000..6187e55841c9 --- /dev/null +++ b/packages/file_selector/file_selector_web/example/README.md @@ -0,0 +1,21 @@ +# Testing + +This package utilizes the `integration_test` package to run its tests in a web browser. + +See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. + +## Running the tests + +Make sure you have updated to the latest Flutter master. + +1. Check what version of Chrome is running on the machine you're running tests on. + +2. Download and install driver for that version from here: + * + +3. Start the driver using `chromedriver --port=4444` + +4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_test.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): + + * Single: `./run_test.sh integration_test/TEST_NAME.dart` + * All: `./run_test.sh` diff --git a/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart b/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart new file mode 100644 index 000000000000..274aed93659e --- /dev/null +++ b/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart @@ -0,0 +1,114 @@ +// 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. + +import 'dart:html'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:file_selector_web/src/dom_helper.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; + +void main() { + group('dom_helper', () { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + late DomHelper domHelper; + late FileUploadInputElement input; + + FileList? createFileList(List files) { + final dataTransfer = DataTransfer(); + files.forEach(dataTransfer.items!.add); + return dataTransfer.files as FileList?; + } + + void setFilesAndTriggerChange(List files) { + input.files = createFileList(files); + input.dispatchEvent(Event('change')); + } + + setUp(() { + domHelper = DomHelper(); + input = FileUploadInputElement(); + }); + + group('getFiles', () { + final mockFile1 = File(['123456'], 'file1.txt'); + final mockFile2 = File([], 'file2.txt'); + + testWidgets('works', (_) async { + final Future> futureFiles = domHelper.getFiles( + input: input, + ); + + setFilesAndTriggerChange([mockFile1, mockFile2]); + + final List files = await futureFiles; + + expect(files.length, 2); + + expect(files[0].name, 'file1.txt'); + expect(await files[0].length(), 6); + expect(await files[0].readAsString(), '123456'); + expect(await files[0].lastModified(), isNotNull); + + expect(files[1].name, 'file2.txt'); + expect(await files[1].length(), 0); + expect(await files[1].readAsString(), ''); + expect(await files[1].lastModified(), isNotNull); + }); + + testWidgets('works multiple times', (_) async { + Future> futureFiles; + List files; + + // It should work the first time + futureFiles = domHelper.getFiles(input: input); + setFilesAndTriggerChange([mockFile1]); + + files = await futureFiles; + + expect(files.length, 1); + expect(files.first.name, mockFile1.name); + + // The same input should work more than once + futureFiles = domHelper.getFiles(input: input); + setFilesAndTriggerChange([mockFile2]); + + files = await futureFiles; + + expect(files.length, 1); + expect(files.first.name, mockFile2.name); + }); + + testWidgets('sets the attributes and clicks it', (_) async { + final accept = '.jpg,.png'; + final multiple = true; + bool wasClicked = false; + + //ignore: unawaited_futures + input.onClick.first.then((_) => wasClicked = true); + + final futureFile = domHelper.getFiles( + accept: accept, + multiple: multiple, + input: input, + ); + + expect(input.matchesWithAncestors('body'), true); + expect(input.accept, accept); + expect(input.multiple, multiple); + expect( + wasClicked, + true, + reason: + 'The should be clicked otherwise no dialog will be shown', + ); + + setFilesAndTriggerChange([]); + await futureFile; + + // It should be already removed from the DOM after the file is resolved. + expect(input.parent, isNull); + }); + }); + }); +} diff --git a/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart b/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart new file mode 100644 index 000000000000..ea9569bf00a9 --- /dev/null +++ b/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart @@ -0,0 +1,121 @@ +// 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. + +import 'dart:html'; +import 'dart:typed_data'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_web/file_selector_web.dart'; +import 'package:file_selector_web/src/dom_helper.dart'; + +void main() { + group('FileSelectorWeb', () { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('openFile', () { + testWidgets('works', (WidgetTester _) async { + final mockFile = createXFile('1001', 'identity.png'); + + final mockDomHelper = MockDomHelper() + ..setFiles([mockFile]) + ..expectAccept('.jpg,.jpeg,image/png,image/*') + ..expectMultiple(false); + + final plugin = FileSelectorWeb(domHelper: mockDomHelper); + + final typeGroup = XTypeGroup( + label: 'images', + extensions: ['jpg', 'jpeg'], + mimeTypes: ['image/png'], + webWildCards: ['image/*'], + ); + + final file = await plugin.openFile(acceptedTypeGroups: [typeGroup]); + + expect(file.name, mockFile.name); + expect(await file.length(), 4); + expect(await file.readAsString(), '1001'); + expect(await file.lastModified(), isNotNull); + }); + }); + + group('openFiles', () { + testWidgets('works', (WidgetTester _) async { + final mockFile1 = createXFile('123456', 'file1.txt'); + final mockFile2 = createXFile('', 'file2.txt'); + + final mockDomHelper = MockDomHelper() + ..setFiles([mockFile1, mockFile2]) + ..expectAccept('.txt') + ..expectMultiple(true); + + final plugin = FileSelectorWeb(domHelper: mockDomHelper); + + final typeGroup = XTypeGroup( + label: 'files', + extensions: ['.txt'], + ); + + final files = await plugin.openFiles(acceptedTypeGroups: [typeGroup]); + + expect(files.length, 2); + + expect(files[0].name, mockFile1.name); + expect(await files[0].length(), 6); + expect(await files[0].readAsString(), '123456'); + expect(await files[0].lastModified(), isNotNull); + + expect(files[1].name, mockFile2.name); + expect(await files[1].length(), 0); + expect(await files[1].readAsString(), ''); + expect(await files[1].lastModified(), isNotNull); + }); + }); + + group('getSavePath', () { + testWidgets('returns non-null', (WidgetTester _) async { + final plugin = FileSelectorWeb(); + final savePath = plugin.getSavePath(); + expect(await savePath, isNotNull); + }); + }); + }); +} + +class MockDomHelper implements DomHelper { + List _files = []; + String _expectedAccept = ''; + bool _expectedMultiple = false; + + @override + Future> getFiles({ + String accept = '', + bool multiple = false, + FileUploadInputElement? input, + }) { + expect(accept, _expectedAccept, + reason: 'Expected "accept" value does not match.'); + expect(multiple, _expectedMultiple, + reason: 'Expected "multiple" value does not match.'); + return Future.value(_files); + } + + void setFiles(List files) { + _files = files; + } + + void expectAccept(String accept) { + _expectedAccept = accept; + } + + void expectMultiple(bool multiple) { + _expectedMultiple = multiple; + } +} + +XFile createXFile(String content, String name) { + final data = Uint8List.fromList(content.codeUnits); + return XFile.fromData(data, name: name, lastModified: DateTime.now()); +} diff --git a/packages/url_launcher/url_launcher_web/test/lib/main.dart b/packages/file_selector/file_selector_web/example/lib/main.dart similarity index 75% rename from packages/url_launcher/url_launcher_web/test/lib/main.dart rename to packages/file_selector/file_selector_web/example/lib/main.dart index 10415204570c..e1a38dcdcd46 100644 --- a/packages/url_launcher/url_launcher_web/test/lib/main.dart +++ b/packages/file_selector/file_selector_web/example/lib/main.dart @@ -17,6 +17,9 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { @override Widget build(BuildContext context) { - return Text('Testing... Look at the console output for results!'); + return Directionality( + textDirection: TextDirection.ltr, + child: Text('Testing... Look at the console output for results!'), + ); } } diff --git a/packages/file_selector/file_selector_web/example/pubspec.yaml b/packages/file_selector/file_selector_web/example/pubspec.yaml new file mode 100644 index 000000000000..cae4b13a8207 --- /dev/null +++ b/packages/file_selector/file_selector_web/example/pubspec.yaml @@ -0,0 +1,21 @@ +name: file_selector_web_integration_tests +publish_to: none + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + build_runner: ^1.10.0 + file_selector_web: + path: ../ + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +environment: + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.27.0-0" # For integration_test from sdk diff --git a/packages/google_sign_in/google_sign_in_web/test/run_test b/packages/file_selector/file_selector_web/example/run_test.sh similarity index 58% rename from packages/google_sign_in/google_sign_in_web/test/run_test rename to packages/file_selector/file_selector_web/example/run_test.sh index 74a8526a0fa3..c9f547a4f7d7 100755 --- a/packages/google_sign_in/google_sign_in_web/test/run_test +++ b/packages/file_selector/file_selector_web/example/run_test.sh @@ -1,17 +1,17 @@ #!/usr/bin/bash + if pgrep -lf chromedriver > /dev/null; then echo "chromedriver is running." if [ $# -eq 0 ]; then echo "No target specified, running all tests..." - find test_driver/ -iname *_integration.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --target='{}' + find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}' else echo "Running test target: $1..." set -x - flutter drive -d web-server --web-port=7357 --browser-name=chrome --target=$1 + flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target=$1 fi else echo "chromedriver is not running." fi - diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_load_integration_test.dart b/packages/file_selector/file_selector_web/example/test_driver/integration_test.dart similarity index 60% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_load_integration_test.dart rename to packages/file_selector/file_selector_web/example/test_driver/integration_test.dart index 39444c0daa24..44d6ed9c64bc 100644 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_load_integration_test.dart +++ b/packages/file_selector/file_selector_web/example/test_driver/integration_test.dart @@ -1,7 +1,7 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// 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. import 'package:integration_test/integration_test_driver.dart'; -Future main() async => integrationDriver(); +Future main() => integrationDriver(); diff --git a/packages/url_launcher/url_launcher_web/test/web/index.html b/packages/file_selector/file_selector_web/example/web/index.html similarity index 100% rename from packages/url_launcher/url_launcher_web/test/web/index.html rename to packages/file_selector/file_selector_web/example/web/index.html diff --git a/packages/file_selector/file_selector_web/lib/file_selector_web.dart b/packages/file_selector/file_selector_web/lib/file_selector_web.dart new file mode 100644 index 000000000000..cf95e403effc --- /dev/null +++ b/packages/file_selector/file_selector_web/lib/file_selector_web.dart @@ -0,0 +1,77 @@ +// 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. + +import 'dart:async'; +import 'package:meta/meta.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_web/src/dom_helper.dart'; +import 'package:file_selector_web/src/utils.dart'; + +/// The web implementation of [FileSelectorPlatform]. +/// +/// This class implements the `package:file_selector` functionality for the web. +class FileSelectorWeb extends FileSelectorPlatform { + final DomHelper _domHelper; + + /// Registers this class as the default instance of [FileSelectorPlatform]. + static void registerWith(Registrar registrar) { + FileSelectorPlatform.instance = FileSelectorWeb(); + } + + /// Default constructor, initializes _domHelper that we can use + /// to interact with the DOM. + /// overrides parameter allows for testing to override functions + FileSelectorWeb({@visibleForTesting DomHelper? domHelper}) + : _domHelper = domHelper ?? DomHelper(); + + @override + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final files = await _openFiles(acceptedTypeGroups: acceptedTypeGroups); + return files.first; + } + + @override + Future> openFiles({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + return _openFiles(acceptedTypeGroups: acceptedTypeGroups, multiple: true); + } + + // This is intended to be passed to XFile, which ignores the path, but 'null' + // indicates a canceled save on other platforms, so provide a non-null dummy + // value. + @override + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) async => + ''; + + @override + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) async => + null; + + Future> _openFiles({ + List? acceptedTypeGroups, + bool multiple = false, + }) async { + final accept = acceptedTypesToString(acceptedTypeGroups); + return _domHelper.getFiles( + accept: accept, + multiple: multiple, + ); + } +} diff --git a/packages/file_selector/file_selector_web/lib/src/dom_helper.dart b/packages/file_selector/file_selector_web/lib/src/dom_helper.dart new file mode 100644 index 000000000000..5c578b6f4639 --- /dev/null +++ b/packages/file_selector/file_selector_web/lib/src/dom_helper.dart @@ -0,0 +1,66 @@ +// 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. + +import 'dart:async'; +import 'dart:html'; +import 'package:meta/meta.dart'; +import 'package:flutter/services.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; + +/// Class to manipulate the DOM with the intention of reading files from it. +class DomHelper { + final _container = Element.tag('file-selector'); + + /// Default constructor, initializes the container DOM element. + DomHelper() { + final body = querySelector('body')!; + body.children.add(_container); + } + + /// Sets the attributes and waits for a file to be selected. + Future> getFiles({ + String accept = '', + bool multiple = false, + @visibleForTesting FileUploadInputElement? input, + }) { + final Completer> completer = Completer(); + final FileUploadInputElement inputElement = + input ?? FileUploadInputElement(); + + _container.children.add( + inputElement + ..accept = accept + ..multiple = multiple, + ); + + inputElement.onChange.first.then((_) { + final List files = + inputElement.files!.map(_convertFileToXFile).toList(); + inputElement.remove(); + completer.complete(files); + }); + + inputElement.onError.first.then((event) { + final ErrorEvent error = event as ErrorEvent; + final platformException = PlatformException( + code: error.type, + message: error.message, + ); + inputElement.remove(); + completer.completeError(platformException); + }); + + inputElement.click(); + + return completer.future; + } + + XFile _convertFileToXFile(File file) => XFile( + Url.createObjectUrl(file), + name: file.name, + length: file.size, + lastModified: DateTime.fromMillisecondsSinceEpoch( + file.lastModified ?? DateTime.now().millisecondsSinceEpoch), + ); +} diff --git a/packages/file_selector/file_selector_web/lib/src/utils.dart b/packages/file_selector/file_selector_web/lib/src/utils.dart new file mode 100644 index 000000000000..6be58c2aa0ec --- /dev/null +++ b/packages/file_selector/file_selector_web/lib/src/utils.dart @@ -0,0 +1,38 @@ +// 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. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; + +/// Convert list of XTypeGroups to a comma-separated string +String acceptedTypesToString(List? acceptedTypes) { + if (acceptedTypes == null) return ''; + final List allTypes = []; + for (final group in acceptedTypes) { + _assertTypeGroupIsValid(group); + if (group.extensions != null) { + allTypes.addAll(group.extensions!.map(_normalizeExtension)); + } + if (group.mimeTypes != null) { + allTypes.addAll(group.mimeTypes!); + } + if (group.webWildCards != null) { + allTypes.addAll(group.webWildCards!); + } + } + return allTypes.join(','); +} + +/// Make sure that at least one of its fields is populated. +void _assertTypeGroupIsValid(XTypeGroup group) { + assert( + !((group.extensions == null || group.extensions!.isEmpty) && + (group.mimeTypes == null || group.mimeTypes!.isEmpty) && + (group.webWildCards == null || group.webWildCards!.isEmpty)), + 'At least one of extensions / mimeTypes / webWildCards is required for web.'); +} + +/// Append a dot at the beggining if it is not there png -> .png +String _normalizeExtension(String ext) { + return ext.isNotEmpty && ext[0] != '.' ? '.' + ext : ext; +} diff --git a/packages/file_selector/file_selector_web/pubspec.yaml b/packages/file_selector/file_selector_web/pubspec.yaml new file mode 100644 index 000000000000..17f0f476cbac --- /dev/null +++ b/packages/file_selector/file_selector_web/pubspec.yaml @@ -0,0 +1,28 @@ +name: file_selector_web +description: Web platform implementation of file_selector +homepage: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector_web +version: 0.8.1 + +flutter: + plugin: + platforms: + web: + pluginClass: FileSelectorWeb + fileName: file_selector_web.dart + +dependencies: + file_selector_platform_interface: ^2.0.0 + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + meta: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.10.0 + +environment: + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/file_selector/file_selector_web/test/more_tests_exist_elsewhere_test.dart b/packages/file_selector/file_selector_web/test/more_tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..e9faa3af4808 --- /dev/null +++ b/packages/file_selector/file_selector_web/test/more_tests_exist_elsewhere_test.dart @@ -0,0 +1,10 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Tell the user where to find the real tests', () { + print('---'); + print('This package also uses integration_test to run additional tests.'); + print('See `example/README.md` for more info.'); + print('---'); + }); +} diff --git a/packages/file_selector/file_selector_web/test/utils_test.dart b/packages/file_selector/file_selector_web/test/utils_test.dart new file mode 100644 index 000000000000..e3e47c00f176 --- /dev/null +++ b/packages/file_selector/file_selector_web/test/utils_test.dart @@ -0,0 +1,57 @@ +// 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. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:file_selector_web/src/utils.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; + +void main() { + group('FileSelectorWeb utils', () { + group('acceptedTypesToString', () { + test('works', () { + final List acceptedTypes = [ + XTypeGroup(label: 'images', webWildCards: ['images/*']), + XTypeGroup(label: 'jpgs', extensions: ['jpg', 'jpeg']), + XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), + ]; + final accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, 'images/*,.jpg,.jpeg,image/png'); + }); + + test('works with an empty list', () { + final List acceptedTypes = []; + final accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, ''); + }); + + test('works with extensions', () { + final List acceptedTypes = [ + XTypeGroup(label: 'jpgs', extensions: ['jpeg', 'jpg']), + XTypeGroup(label: 'pngs', extensions: ['png']), + ]; + final accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, '.jpeg,.jpg,.png'); + }); + + test('works with mime types', () { + final List acceptedTypes = [ + XTypeGroup(label: 'jpgs', mimeTypes: ['image/jpeg', 'image/jpg']), + XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), + ]; + final accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, 'image/jpeg,image/jpg,image/png'); + }); + + test('works with web wild cards', () { + final List acceptedTypes = [ + XTypeGroup(label: 'images', webWildCards: ['image/*']), + XTypeGroup(label: 'audios', webWildCards: ['audio/*']), + XTypeGroup(label: 'videos', webWildCards: ['video/*']), + ]; + final accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, 'image/*,audio/*,video/*'); + }); + }); + }); +} diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 401be5f5278a..08f137c09434 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.0.0 + +* Bump Dart SDK for null-safety compatibility. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 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/ios/.gitignore b/packages/flutter_plugin_android_lifecycle/example/ios/.gitignore deleted file mode 100644 index f78c1480b6dd..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Generated.xcconfig -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/AppFrameworkInfo.plist b/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6b4c0f78a785..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 8.0 - - diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/Debug.xcconfig b/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/Release.xcconfig b/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner.xcodeproj/project.pbxproj b/packages/flutter_plugin_android_lifecycle/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index f42fc07b1c19..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,576 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* 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, ); }; }; - 68BFFC9A2252F6377926CCB6 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D97B2D435F77384E1832544A /* 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 */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 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 = ""; }; - 48B2B2D61E102CB7FCA66327 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 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 = ""; }; - 8A495AA36DFBF39C3BD5D917 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; 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 = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9E27BB0D8AE008E9718C1EC3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - D97B2D435F77384E1832544A /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 68BFFC9A2252F6377926CCB6 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 29946A38AEAEDCD95716766D /* Pods */ = { - isa = PBXGroup; - children = ( - 8A495AA36DFBF39C3BD5D917 /* Pods-Runner.debug.xcconfig */, - 9E27BB0D8AE008E9718C1EC3 /* Pods-Runner.release.xcconfig */, - 48B2B2D61E102CB7FCA66327 /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 29946A38AEAEDCD95716766D /* Pods */, - C6B60E52AC0C0C398A9D6E3E /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - C6B60E52AC0C0C398A9D6E3E /* Frameworks */ = { - isa = PBXGroup; - children = ( - D97B2D435F77384E1832544A /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - B0349D7BFB658C43C3407041 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 2D345E120F865FCD8BCE231E /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1020; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 2D345E120F865FCD8BCE231E /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - 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; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - B0349D7BFB658C43C3407041 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.flutterAndroidLifecycleExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.flutterAndroidLifecycleExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.flutterAndroidLifecycleExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/flutter_plugin_android_lifecycle/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index a28140cfdb3f..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/AppDelegate.h b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bbf9cf4..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/AppDelegate.m b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 59a72e90be12..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fab2d9d..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4725e9..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2fd467..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eacad3b..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eacad3b..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eacad3b..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b70f1..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7c939..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Base.lproj/Main.storyboard b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Info.plist b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Info.plist deleted file mode 100644 index 8526e1f7226c..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Info.plist +++ /dev/null @@ -1,45 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - flutter_plugin_android_lifecycle_example - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/main.m b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/main.m deleted file mode 100644 index dff6597e4513..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} 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..833c2eba4319 100644 --- a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml @@ -1,23 +1,26 @@ 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: + # When depending on this package from a real application you should use: + # flutter_plugin_android_lifecycle: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ 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/ios/.gitignore b/packages/flutter_plugin_android_lifecycle/ios/.gitignore deleted file mode 100644 index aa479fd3ce8a..000000000000 --- a/packages/flutter_plugin_android_lifecycle/ios/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/flutter_plugin_android_lifecycle/ios/Assets/.gitkeep b/packages/flutter_plugin_android_lifecycle/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/flutter_plugin_android_lifecycle/ios/Classes/FlutterAndroidLifecyclePlugin.m b/packages/flutter_plugin_android_lifecycle/ios/Classes/FlutterAndroidLifecyclePlugin.m deleted file mode 100644 index 38cffd362da7..000000000000 --- a/packages/flutter_plugin_android_lifecycle/ios/Classes/FlutterAndroidLifecyclePlugin.m +++ /dev/null @@ -1,10 +0,0 @@ -// 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 "FlutterAndroidLifecyclePlugin.h" - -@implementation FlutterAndroidLifecyclePlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { -} -@end diff --git a/packages/flutter_plugin_android_lifecycle/ios/flutter_plugin_android_lifecycle.podspec b/packages/flutter_plugin_android_lifecycle/ios/flutter_plugin_android_lifecycle.podspec deleted file mode 100644 index 0c802a3101ba..000000000000 --- a/packages/flutter_plugin_android_lifecycle/ios/flutter_plugin_android_lifecycle.podspec +++ /dev/null @@ -1,26 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint flutter_plugin_android_lifecycle.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'flutter_plugin_android_lifecycle' - s.version = '0.0.1' - s.summary = 'Flutter Android Lifecycle Plugin' - s.description = <<-DESC -A Flutter plugin for Android to allow other Flutter plugins to access Android Lifecycle objects in the plugin's binding. -This plugin a no-op on iOS. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/flutter_plugin_android_lifecycle' } - s.documentation_url = 'https://pub.dev/packages/flutter_plugin_android_lifecycle' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/flutter_plugin_android_lifecycle/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/pubspec.yaml index a671381b3f47..fc2805ef814b 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 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-259.9.beta <3.0.0" + flutter: ">=1.20.0" 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 4a7026f01ccd..dae5caf89a60 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,39 @@ +## 2.0.0 + +* Migrate to null-safety +* BREAKING CHANGE: Passing an unknown map object ID (e.g., MarkerId) to a + method, it will throw an `UnknownMapObjectIDError`. Previously it would + either silently do nothing, or throw an error trying to call a function on + `null`, depneding on the method. + +## 1.2.0 + +* Support custom tiles. + +## 1.1.1 + +* Fix in example app to properly place polyline at initial camera position. + +## 1.1.0 + +* Add support for holes in Polygons. + +## 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. + ## 1.0.6 * Update Dart SDK constraint in example. 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/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle index a1d7da08a8d9..479c100b8293 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter/android/build.gradle @@ -39,6 +39,11 @@ android { androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } dependencies { diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 5ef7b26a4a2c..f9e0ed9c32d0 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -23,6 +23,7 @@ import com.google.android.gms.maps.model.PatternItem; import com.google.android.gms.maps.model.RoundCap; import com.google.android.gms.maps.model.SquareCap; +import com.google.android.gms.maps.model.Tile; import io.flutter.view.FlutterMain; import java.util.ArrayList; import java.util.Arrays; @@ -33,6 +34,9 @@ /** Conversions between JSON-like values and GoogleMaps data types. */ class Convert { + // TODO(hamdikahloun): FlutterMain has been deprecated and should be replaced with FlutterLoader + // when it's available in Stable channel: https://github.com/flutter/flutter/issues/70923. + @SuppressWarnings("deprecation") private static BitmapDescriptor toBitmapDescriptor(Object o) { final List data = toList(o); switch (toString(data.get(0))) { @@ -75,7 +79,8 @@ private static BitmapDescriptor getBitmapFromBytes(List data) { } } else { throw new IllegalArgumentException( - "fromBytes should have exactly one argument, the bytes. Got: " + data.size()); + "fromBytes should have exactly one argument, interpretTileOverlayOptions the bytes. Got: " + + data.size()); } } @@ -197,6 +202,20 @@ static Object circleIdToJson(String circleId) { return data; } + static Map tileOverlayArgumentsToJson( + String tileOverlayId, int x, int y, int zoom) { + + if (tileOverlayId == null) { + return null; + } + final Map data = new HashMap<>(4); + data.put("tileOverlayId", tileOverlayId); + data.put("x", x); + data.put("y", y); + data.put("zoom", zoom); + return data; + } + static Object latLngToJson(LatLng latLng) { return Arrays.asList(latLng.latitude, latLng.longitude); } @@ -207,8 +226,9 @@ static LatLng toLatLng(Object o) { } static Point toPoint(Object o) { - Map screenCoordinate = (Map) o; - return new Point(screenCoordinate.get("x"), screenCoordinate.get("y")); + Object x = toMap(o).get("x"); + Object y = toMap(o).get("y"); + return new Point((int) x, (int) y); } static Map pointToJson(Point point) { @@ -234,6 +254,18 @@ private static List toList(Object o) { return (Map) o; } + private static Map toObjectMap(Object o) { + Map hashMap = new HashMap<>(); + Map map = (Map) o; + for (Object key : map.keySet()) { + Object object = map.get(key); + if (object != null) { + hashMap.put((String) key, object); + } + } + return hashMap; + } + private static float toFractionalPixels(Object o, float density) { return toFloat(o) * density; } @@ -377,7 +409,7 @@ static String interpretMarkerOptions(Object o, MarkerOptionsSink sink) { final Object infoWindow = data.get("infoWindow"); if (infoWindow != null) { - interpretInfoWindowOptions(sink, (Map) infoWindow); + interpretInfoWindowOptions(sink, toObjectMap(infoWindow)); } final Object position = data.get("position"); if (position != null) { @@ -452,6 +484,10 @@ static String interpretPolygonOptions(Object o, PolygonOptionsSink sink) { if (points != null) { sink.setPoints(toPoints(points)); } + final Object holes = data.get("holes"); + if (holes != null) { + sink.setHoles(toHoles(holes)); + } final String polygonId = (String) data.get("polygonId"); if (polygonId == null) { throw new IllegalArgumentException("polygonId was null"); @@ -560,13 +596,23 @@ private static List toPoints(Object o) { final List data = toList(o); final List points = new ArrayList<>(data.size()); - for (Object ob : data) { - final List point = toList(ob); + for (Object rawPoint : data) { + final List point = toList(rawPoint); points.add(new LatLng(toFloat(point.get(0)), toFloat(point.get(1)))); } return points; } + private static List> toHoles(Object o) { + final List data = toList(o); + final List> holes = new ArrayList<>(data.size()); + + for (Object rawHole : data) { + holes.add(toPoints(rawHole)); + } + return holes; + } + private static List toPattern(Object o) { final List data = toList(o); @@ -615,4 +661,39 @@ private static Cap toCap(Object o) { throw new IllegalArgumentException("Cannot interpret " + o + " as Cap"); } } + + static String interpretTileOverlayOptions(Map data, TileOverlaySink sink) { + final Object fadeIn = data.get("fadeIn"); + if (fadeIn != null) { + sink.setFadeIn(toBoolean(fadeIn)); + } + final Object transparency = data.get("transparency"); + if (transparency != null) { + sink.setTransparency(toFloat(transparency)); + } + final Object zIndex = data.get("zIndex"); + if (zIndex != null) { + sink.setZIndex(toFloat(zIndex)); + } + final Object visible = data.get("visible"); + if (visible != null) { + sink.setVisible(toBoolean(visible)); + } + final String tileOverlayId = (String) data.get("tileOverlayId"); + if (tileOverlayId == null) { + throw new IllegalArgumentException("tileOverlayId was null"); + } else { + return tileOverlayId; + } + } + + static Tile interpretTile(Map data) { + int width = toInt(data.get("width")); + int height = toInt(data.get("height")); + byte[] dataArray = null; + if (data.get("data") != null) { + dataArray = (byte[]) data.get("data"); + } + return new Tile(width, height, dataArray); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java index 93a3c3ec9c28..6d5c8c64ae1d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java @@ -10,6 +10,8 @@ import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLngBounds; import io.flutter.plugin.common.BinaryMessenger; +import java.util.List; +import java.util.Map; class GoogleMapBuilder implements GoogleMapOptionsSink { private final GoogleMapOptions options = new GoogleMapOptions(); @@ -23,6 +25,7 @@ class GoogleMapBuilder implements GoogleMapOptionsSink { private Object initialPolygons; private Object initialPolylines; private Object initialCircles; + private List> initialTileOverlays; private Rect padding = new Rect(0, 0, 0, 0); GoogleMapController build( @@ -44,6 +47,7 @@ GoogleMapController build( controller.setInitialPolylines(initialPolylines); controller.setInitialCircles(initialCircles); controller.setPadding(padding.top, padding.left, padding.bottom, padding.right); + controller.setInitialTileOverlays(initialTileOverlays); return controller; } @@ -165,4 +169,9 @@ public void setInitialPolylines(Object initialPolylines) { public void setInitialCircles(Object initialCircles) { this.initialCircles = initialCircles; } + + @Override + public void setInitialTileOverlays(List> initialTileOverlays) { + this.initialTileOverlays = initialTileOverlays; + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index 33cacffc88d4..7db65c56a5e8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -76,10 +76,12 @@ final class GoogleMapController private final PolygonsController polygonsController; private final PolylinesController polylinesController; private final CirclesController circlesController; + private final TileOverlaysController tileOverlaysController; private List initialMarkers; private List initialPolygons; private List initialPolylines; private List initialCircles; + private List> initialTileOverlays; GoogleMapController( int id, @@ -99,6 +101,7 @@ final class GoogleMapController this.polygonsController = new PolygonsController(methodChannel, density); this.polylinesController = new PolylinesController(methodChannel, density); this.circlesController = new CirclesController(methodChannel, density); + this.tileOverlaysController = new TileOverlaysController(methodChannel); } @Override @@ -140,10 +143,12 @@ public void onMapReady(GoogleMap googleMap) { polygonsController.setGoogleMap(googleMap); polylinesController.setGoogleMap(googleMap); circlesController.setGoogleMap(googleMap); + tileOverlaysController.setGoogleMap(googleMap); updateInitialMarkers(); updateInitialPolygons(); updateInitialPolylines(); updateInitialCircles(); + updateInitialTileOverlays(); } @Override @@ -239,12 +244,12 @@ public void onSnapshotReady(Bitmap bitmap) { } case "markers#update": { - Object markersToAdd = call.argument("markersToAdd"); - markersController.addMarkers((List) markersToAdd); - Object markersToChange = call.argument("markersToChange"); - markersController.changeMarkers((List) markersToChange); - Object markerIdsToRemove = call.argument("markerIdsToRemove"); - markersController.removeMarkers((List) markerIdsToRemove); + List markersToAdd = call.argument("markersToAdd"); + markersController.addMarkers(markersToAdd); + List markersToChange = call.argument("markersToChange"); + markersController.changeMarkers(markersToChange); + List markerIdsToRemove = call.argument("markerIdsToRemove"); + markersController.removeMarkers(markerIdsToRemove); result.success(null); break; } @@ -268,34 +273,34 @@ public void onSnapshotReady(Bitmap bitmap) { } case "polygons#update": { - Object polygonsToAdd = call.argument("polygonsToAdd"); - polygonsController.addPolygons((List) polygonsToAdd); - Object polygonsToChange = call.argument("polygonsToChange"); - polygonsController.changePolygons((List) polygonsToChange); - Object polygonIdsToRemove = call.argument("polygonIdsToRemove"); - polygonsController.removePolygons((List) polygonIdsToRemove); + List polygonsToAdd = call.argument("polygonsToAdd"); + polygonsController.addPolygons(polygonsToAdd); + List polygonsToChange = call.argument("polygonsToChange"); + polygonsController.changePolygons(polygonsToChange); + List polygonIdsToRemove = call.argument("polygonIdsToRemove"); + polygonsController.removePolygons(polygonIdsToRemove); result.success(null); break; } case "polylines#update": { - Object polylinesToAdd = call.argument("polylinesToAdd"); - polylinesController.addPolylines((List) polylinesToAdd); - Object polylinesToChange = call.argument("polylinesToChange"); - polylinesController.changePolylines((List) polylinesToChange); - Object polylineIdsToRemove = call.argument("polylineIdsToRemove"); - polylinesController.removePolylines((List) polylineIdsToRemove); + List polylinesToAdd = call.argument("polylinesToAdd"); + polylinesController.addPolylines(polylinesToAdd); + List polylinesToChange = call.argument("polylinesToChange"); + polylinesController.changePolylines(polylinesToChange); + List polylineIdsToRemove = call.argument("polylineIdsToRemove"); + polylinesController.removePolylines(polylineIdsToRemove); result.success(null); break; } case "circles#update": { - Object circlesToAdd = call.argument("circlesToAdd"); - circlesController.addCircles((List) circlesToAdd); - Object circlesToChange = call.argument("circlesToChange"); - circlesController.changeCircles((List) circlesToChange); - Object circleIdsToRemove = call.argument("circleIdsToRemove"); - circlesController.removeCircles((List) circleIdsToRemove); + List circlesToAdd = call.argument("circlesToAdd"); + circlesController.addCircles(circlesToAdd); + List circlesToChange = call.argument("circlesToChange"); + circlesController.changeCircles(circlesToChange); + List circleIdsToRemove = call.argument("circleIdsToRemove"); + circlesController.removeCircles(circleIdsToRemove); result.success(null); break; } @@ -385,6 +390,30 @@ public void onSnapshotReady(Bitmap bitmap) { result.success(mapStyleResult); break; } + case "tileOverlays#update": + { + List> tileOverlaysToAdd = call.argument("tileOverlaysToAdd"); + tileOverlaysController.addTileOverlays(tileOverlaysToAdd); + List> tileOverlaysToChange = call.argument("tileOverlaysToChange"); + tileOverlaysController.changeTileOverlays(tileOverlaysToChange); + List tileOverlaysToRemove = call.argument("tileOverlayIdsToRemove"); + tileOverlaysController.removeTileOverlays(tileOverlaysToRemove); + result.success(null); + break; + } + case "tileOverlays#clearTileCache": + { + String tileOverlayId = call.argument("tileOverlayId"); + tileOverlaysController.clearTileCache(tileOverlayId); + result.success(null); + break; + } + case "map#getTileOverlayInfo": + { + String tileOverlayId = call.argument("tileOverlayId"); + result.success(tileOverlaysController.getTileOverlayInfo(tileOverlayId)); + break; + } default: result.notImplemented(); } @@ -682,7 +711,8 @@ public void setZoomControlsEnabled(boolean zoomControlsEnabled) { @Override public void setInitialMarkers(Object initialMarkers) { - this.initialMarkers = (List) initialMarkers; + ArrayList markers = (ArrayList) initialMarkers; + this.initialMarkers = markers != null ? new ArrayList<>(markers) : null; if (googleMap != null) { updateInitialMarkers(); } @@ -694,7 +724,8 @@ private void updateInitialMarkers() { @Override public void setInitialPolygons(Object initialPolygons) { - this.initialPolygons = (List) initialPolygons; + ArrayList polygons = (ArrayList) initialPolygons; + this.initialPolygons = polygons != null ? new ArrayList<>(polygons) : null; if (googleMap != null) { updateInitialPolygons(); } @@ -706,7 +737,8 @@ private void updateInitialPolygons() { @Override public void setInitialPolylines(Object initialPolylines) { - this.initialPolylines = (List) initialPolylines; + ArrayList polylines = (ArrayList) initialPolylines; + this.initialPolylines = polylines != null ? new ArrayList<>(polylines) : null; if (googleMap != null) { updateInitialPolylines(); } @@ -718,7 +750,8 @@ private void updateInitialPolylines() { @Override public void setInitialCircles(Object initialCircles) { - this.initialCircles = (List) initialCircles; + ArrayList circles = (ArrayList) initialCircles; + this.initialCircles = circles != null ? new ArrayList<>(circles) : null; if (googleMap != null) { updateInitialCircles(); } @@ -728,6 +761,18 @@ private void updateInitialCircles() { circlesController.addCircles(initialCircles); } + @Override + public void setInitialTileOverlays(List> initialTileOverlays) { + this.initialTileOverlays = initialTileOverlays; + if (googleMap != null) { + updateInitialTileOverlays(); + } + } + + private void updateInitialTileOverlays() { + tileOverlaysController.addTileOverlays(initialTileOverlays); + } + @SuppressLint("MissingPermission") private void updateMyLocationSettings() { if (hasLocationPermission()) { diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java index e56adbbb987a..bf9188ffd1b1 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java @@ -10,6 +10,7 @@ import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.platform.PlatformView; import io.flutter.plugin.platform.PlatformViewFactory; +import java.util.List; import java.util.Map; public class GoogleMapFactory extends PlatformViewFactory { @@ -46,6 +47,9 @@ public PlatformView create(Context context, int id, Object args) { if (params.containsKey("circlesToAdd")) { builder.setInitialCircles(params.get("circlesToAdd")); } + if (params.containsKey("tileOverlaysToAdd")) { + builder.setInitialTileOverlays((List>) params.get("tileOverlaysToAdd")); + } return builder.build(id, context, binaryMessenger, lifecycleProvider); } } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java index 9e6fa2a27236..03377d4b760e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java @@ -5,6 +5,8 @@ package io.flutter.plugins.googlemaps; import com.google.android.gms.maps.model.LatLngBounds; +import java.util.List; +import java.util.Map; /** Receiver of GoogleMap configuration options. */ interface GoogleMapOptionsSink { @@ -51,4 +53,6 @@ interface GoogleMapOptionsSink { void setInitialPolylines(Object initialPolylines); void setInitialCircles(Object initialCircles); + + void setInitialTileOverlays(List> initialTileOverlays); } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java index 600762afe4ee..7691e58e4ae6 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java @@ -41,6 +41,13 @@ public void setPoints(List points) { polygonOptions.addAll(points); } + @Override + public void setHoles(List> holes) { + for (List hole : holes) { + polygonOptions.addHole(hole); + } + } + @Override public void setConsumeTapEvents(boolean consumeTapEvents) { this.consumeTapEvents = consumeTapEvents; diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java index adb01b8a490a..43f1bfd7ec4c 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java @@ -52,6 +52,10 @@ public void setPoints(List points) { polygon.setPoints(points); } + public void setHoles(List> holes) { + polygon.setHoles(holes); + } + @Override public void setVisible(boolean visible) { polygon.setVisible(visible); diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java index df4dae0fda4e..2985a7b762e5 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java @@ -20,6 +20,8 @@ interface PolygonOptionsSink { void setPoints(List points); + void setHoles(List> holes); + void setVisible(boolean visible); void setStrokeWidth(float width); diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java new file mode 100644 index 000000000000..1b5593358536 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java @@ -0,0 +1,46 @@ +// 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. + +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.TileOverlayOptions; +import com.google.android.gms.maps.model.TileProvider; + +class TileOverlayBuilder implements TileOverlaySink { + + private final TileOverlayOptions tileOverlayOptions; + + TileOverlayBuilder() { + this.tileOverlayOptions = new TileOverlayOptions(); + } + + TileOverlayOptions build() { + return tileOverlayOptions; + } + + @Override + public void setFadeIn(boolean fadeIn) { + tileOverlayOptions.fadeIn(fadeIn); + } + + @Override + public void setTransparency(float transparency) { + tileOverlayOptions.transparency(transparency); + } + + @Override + public void setZIndex(float zIndex) { + tileOverlayOptions.zIndex(zIndex); + } + + @Override + public void setVisible(boolean visible) { + tileOverlayOptions.visible(visible); + } + + @Override + public void setTileProvider(TileProvider tileProvider) { + tileOverlayOptions.tileProvider(tileProvider); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java new file mode 100644 index 000000000000..1204bcdaafd7 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java @@ -0,0 +1,62 @@ +// 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. + +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.TileOverlay; +import com.google.android.gms.maps.model.TileProvider; +import java.util.HashMap; +import java.util.Map; + +class TileOverlayController implements TileOverlaySink { + + private final TileOverlay tileOverlay; + + TileOverlayController(TileOverlay tileOverlay) { + this.tileOverlay = tileOverlay; + } + + void remove() { + tileOverlay.remove(); + } + + void clearTileCache() { + tileOverlay.clearTileCache(); + } + + Map getTileOverlayInfo() { + Map tileOverlayInfo = new HashMap<>(); + tileOverlayInfo.put("fadeIn", tileOverlay.getFadeIn()); + tileOverlayInfo.put("transparency", tileOverlay.getTransparency()); + tileOverlayInfo.put("id", tileOverlay.getId()); + tileOverlayInfo.put("zIndex", tileOverlay.getZIndex()); + tileOverlayInfo.put("visible", tileOverlay.isVisible()); + return tileOverlayInfo; + } + + @Override + public void setFadeIn(boolean fadeIn) { + tileOverlay.setFadeIn(fadeIn); + } + + @Override + public void setTransparency(float transparency) { + tileOverlay.setTransparency(transparency); + } + + @Override + public void setZIndex(float zIndex) { + tileOverlay.setZIndex(zIndex); + } + + @Override + public void setVisible(boolean visible) { + tileOverlay.setVisible(visible); + } + + @Override + public void setTileProvider(TileProvider tileProvider) { + // You can not change tile provider after creation + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java new file mode 100644 index 000000000000..fd611d7c57f3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java @@ -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. + +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.TileProvider; + +/** Receiver of TileOverlayOptions configuration. */ +interface TileOverlaySink { + void setFadeIn(boolean fadeIn); + + void setTransparency(float transparency); + + void setZIndex(float zIndex); + + void setVisible(boolean visible); + + void setTileProvider(TileProvider tileProvider); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java new file mode 100644 index 000000000000..cd583e247cdd --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java @@ -0,0 +1,120 @@ +// 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. + +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.TileOverlay; +import com.google.android.gms.maps.model.TileOverlayOptions; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class TileOverlaysController { + + private final Map tileOverlayIdToController; + private final MethodChannel methodChannel; + private GoogleMap googleMap; + + TileOverlaysController(MethodChannel methodChannel) { + this.tileOverlayIdToController = new HashMap<>(); + this.methodChannel = methodChannel; + } + + void setGoogleMap(GoogleMap googleMap) { + this.googleMap = googleMap; + } + + void addTileOverlays(List> tileOverlaysToAdd) { + if (tileOverlaysToAdd == null) { + return; + } + for (Map tileOverlayToAdd : tileOverlaysToAdd) { + addTileOverlay(tileOverlayToAdd); + } + } + + void changeTileOverlays(List> tileOverlaysToChange) { + if (tileOverlaysToChange == null) { + return; + } + for (Map tileOverlayToChange : tileOverlaysToChange) { + changeTileOverlay(tileOverlayToChange); + } + } + + void removeTileOverlays(List tileOverlayIdsToRemove) { + if (tileOverlayIdsToRemove == null) { + return; + } + for (String tileOverlayId : tileOverlayIdsToRemove) { + if (tileOverlayId == null) { + continue; + } + removeTileOverlay(tileOverlayId); + } + } + + void clearTileCache(String tileOverlayId) { + if (tileOverlayId == null) { + return; + } + TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId); + if (tileOverlayController != null) { + tileOverlayController.clearTileCache(); + } + } + + Map getTileOverlayInfo(String tileOverlayId) { + if (tileOverlayId == null) { + return null; + } + TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId); + if (tileOverlayController == null) { + return null; + } + return tileOverlayController.getTileOverlayInfo(); + } + + private void addTileOverlay(Map tileOverlayOptions) { + if (tileOverlayOptions == null) { + return; + } + TileOverlayBuilder tileOverlayOptionsBuilder = new TileOverlayBuilder(); + String tileOverlayId = + Convert.interpretTileOverlayOptions(tileOverlayOptions, tileOverlayOptionsBuilder); + TileProviderController tileProviderController = + new TileProviderController(methodChannel, tileOverlayId); + tileOverlayOptionsBuilder.setTileProvider(tileProviderController); + TileOverlayOptions options = tileOverlayOptionsBuilder.build(); + TileOverlay tileOverlay = googleMap.addTileOverlay(options); + TileOverlayController tileOverlayController = new TileOverlayController(tileOverlay); + tileOverlayIdToController.put(tileOverlayId, tileOverlayController); + } + + private void changeTileOverlay(Map tileOverlayOptions) { + if (tileOverlayOptions == null) { + return; + } + String tileOverlayId = getTileOverlayId(tileOverlayOptions); + TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId); + if (tileOverlayController != null) { + Convert.interpretTileOverlayOptions(tileOverlayOptions, tileOverlayController); + } + } + + private void removeTileOverlay(String tileOverlayId) { + TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId); + if (tileOverlayController != null) { + tileOverlayController.remove(); + tileOverlayIdToController.remove(tileOverlayId); + } + } + + @SuppressWarnings("unchecked") + private static String getTileOverlayId(Map tileOverlay) { + return (String) tileOverlay.get("tileOverlayId"); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java new file mode 100644 index 000000000000..f28118c91db4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java @@ -0,0 +1,100 @@ +// 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. + +package io.flutter.plugins.googlemaps; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.android.gms.maps.model.Tile; +import com.google.android.gms.maps.model.TileProvider; +import io.flutter.plugin.common.MethodChannel; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +class TileProviderController implements TileProvider { + + private static final String TAG = "TileProviderController"; + + private final String tileOverlayId; + private final MethodChannel methodChannel; + private final Handler handler = new Handler(Looper.getMainLooper()); + + TileProviderController(MethodChannel methodChannel, String tileOverlayId) { + this.tileOverlayId = tileOverlayId; + this.methodChannel = methodChannel; + } + + @Override + public Tile getTile(final int x, final int y, final int zoom) { + Worker worker = new Worker(x, y, zoom); + return worker.getTile(); + } + + private final class Worker implements MethodChannel.Result { + + private final CountDownLatch countDownLatch = new CountDownLatch(1); + private final int x; + private final int y; + private final int zoom; + private Map result; + + Worker(int x, int y, int zoom) { + this.x = x; + this.y = y; + this.zoom = zoom; + } + + @NonNull + Tile getTile() { + handler.post( + () -> + methodChannel.invokeMethod( + "tileOverlay#getTile", + Convert.tileOverlayArgumentsToJson(tileOverlayId, x, y, zoom), + this)); + try { + // Because `methodChannel.invokeMethod` is async, we use a `countDownLatch` make it synchronized. + countDownLatch.await(); + } catch (InterruptedException e) { + Log.e( + TAG, + String.format("countDownLatch: can't get tile: x = %d, y= %d, zoom = %d", x, y, zoom), + e); + return TileProvider.NO_TILE; + } + try { + return Convert.interpretTile(result); + } catch (Exception e) { + Log.e(TAG, "Can't parse tile data", e); + return TileProvider.NO_TILE; + } + } + + @Override + public void success(Object data) { + result = (Map) data; + countDownLatch.countDown(); + } + + @Override + public void error(String errorCode, String errorMessage, Object data) { + Log.e( + TAG, + String.format( + "Can't get tile: errorCode = %s, errorMessage = %s, date = %s", + errorCode, errorCode, data)); + result = null; + countDownLatch.countDown(); + } + + @Override + public void notImplemented() { + Log.e(TAG, "Can't get tile: notImplemented"); + result = null; + countDownLatch.countDown(); + } + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart index 2fcfec15713a..a5025590d72a 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart @@ -1,6 +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. +// @dart=2.9 import 'dart:typed_data'; import 'package:flutter/services.dart'; @@ -76,4 +77,11 @@ class GoogleMapInspector { Future takeSnapshot() async { return await _channel.invokeMethod('map#takeSnapshot'); } + + Future> getTileOverlayInfo(String id) async { + return await _channel.invokeMapMethod( + 'map#getTileOverlayInfo', { + 'tileOverlayId': id, + }); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart index 2a5bf80a4578..01b0c9b03e68 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart @@ -1,10 +1,12 @@ // 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. +// @dart=2.9 import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; +import 'dart:ui' as ui; import 'package:integration_test/integration_test.dart'; import 'package:flutter/material.dart'; @@ -953,10 +955,8 @@ void main() { final BitmapDescriptor scaled = await BitmapDescriptor.fromAssetImage( imageConfiguration, 'red_square.png', mipmaps: false); - // ignore: invalid_use_of_visible_for_testing_member - expect(mip.toJson()[2], 1); - // ignore: invalid_use_of_visible_for_testing_member - expect(scaled.toJson()[2], 2); + expect((mip.toJson() as List)[2], 1); + expect((scaled.toJson() as List)[2], 2); }); testWidgets('testTakeSnapshot', (WidgetTester tester) async { @@ -987,4 +987,247 @@ void main() { // TODO(cyanglaz): un-skip the test when we can test this on CI with API key enabled. // https://github.com/flutter/flutter/issues/57057 skip: Platform.isAndroid); + + testWidgets( + 'set tileOverlay correctly', + (WidgetTester tester) async { + Completer inspectorCompleter = + Completer(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 2, + visible: true, + transparency: 0.2, + fadeIn: true, + ); + + final TileOverlay tileOverlay2 = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_2'), + tileProvider: _DebugTileProvider(), + zIndex: 1, + visible: false, + transparency: 0.3, + fadeIn: false, + ); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1, tileOverlay2}, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel); + inspectorCompleter.complete(inspector); + }, + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + + Map tileOverlayInfo1 = + await inspector.getTileOverlayInfo('tile_overlay_1'); + Map tileOverlayInfo2 = + await inspector.getTileOverlayInfo('tile_overlay_2'); + + expect(tileOverlayInfo1['visible'], isTrue); + expect(tileOverlayInfo1['fadeIn'], isTrue); + expect(tileOverlayInfo1['transparency'], + moreOrLessEquals(0.2, epsilon: 0.001)); + expect(tileOverlayInfo1['zIndex'], 2); + + expect(tileOverlayInfo2['visible'], isFalse); + expect(tileOverlayInfo2['fadeIn'], isFalse); + expect(tileOverlayInfo2['transparency'], + moreOrLessEquals(0.3, epsilon: 0.001)); + expect(tileOverlayInfo2['zIndex'], 1); + }, + ); + + testWidgets( + 'update tileOverlays correctly', + (WidgetTester tester) async { + Completer inspectorCompleter = + Completer(); + final Key key = GlobalKey(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 2, + visible: true, + transparency: 0.2, + fadeIn: true, + ); + + final TileOverlay tileOverlay2 = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_2'), + tileProvider: _DebugTileProvider(), + zIndex: 3, + visible: true, + transparency: 0.5, + fadeIn: true, + ); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1, tileOverlay2}, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel); + inspectorCompleter.complete(inspector); + }, + ), + ), + ); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + + final TileOverlay tileOverlay1New = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 1, + visible: false, + transparency: 0.3, + fadeIn: false, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1New}, + onMapCreated: (GoogleMapController controller) { + fail('update: OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + Map tileOverlayInfo1 = + await inspector.getTileOverlayInfo('tile_overlay_1'); + Map tileOverlayInfo2 = + await inspector.getTileOverlayInfo('tile_overlay_2'); + + expect(tileOverlayInfo1['visible'], isFalse); + expect(tileOverlayInfo1['fadeIn'], isFalse); + expect(tileOverlayInfo1['transparency'], + moreOrLessEquals(0.3, epsilon: 0.001)); + expect(tileOverlayInfo1['zIndex'], 1); + + expect(tileOverlayInfo2, isNull); + }, + ); + + testWidgets( + 'remove tileOverlays correctly', + (WidgetTester tester) async { + Completer inspectorCompleter = + Completer(); + final Key key = GlobalKey(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 2, + visible: true, + transparency: 0.2, + fadeIn: true, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1}, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel); + inspectorCompleter.complete(inspector); + }, + ), + ), + ); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + Map tileOverlayInfo1 = + await inspector.getTileOverlayInfo('tile_overlay_1'); + + expect(tileOverlayInfo1, isNull); + }, + ); +} + +class _DebugTileProvider implements TileProvider { + _DebugTileProvider() { + boxPaint.isAntiAlias = true; + boxPaint.color = Colors.blue; + boxPaint.strokeWidth = 2.0; + boxPaint.style = PaintingStyle.stroke; + } + + static const int width = 100; + static const int height = 100; + static final Paint boxPaint = Paint(); + static final TextStyle textStyle = TextStyle( + color: Colors.red, + fontSize: 20, + ); + + @override + Future getTile(int x, int y, int zoom) async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final TextSpan textSpan = TextSpan( + text: "$x,$y", + style: textStyle, + ); + final TextPainter textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + textPainter.layout( + minWidth: 0.0, + maxWidth: width.toDouble(), + ); + final Offset offset = const Offset(0, 0); + textPainter.paint(canvas, offset); + canvas.drawRect( + Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); + final ui.Picture picture = recorder.endRecording(); + final Uint8List byteData = await picture + .toImage(width, height) + .then((ui.Image image) => + image.toByteData(format: ui.ImageByteFormat.png)) + .then((ByteData byteData) => byteData.buffer.asUint8List()); + return Tile(width, height, byteData); + } } 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/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist index 372490e1a367..0fa9c73c5d42 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist @@ -47,7 +47,5 @@ UIViewControllerBasedStatusBarAppearance - io.flutter.embedded_views_preview - 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..f6a2fecf5b0d 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 @@ -26,7 +26,7 @@ class AnimateCamera extends StatefulWidget { } class AnimateCameraState extends State { - GoogleMapController mapController; + GoogleMapController? mapController; void _onMapCreated(GoogleMapController controller) { mapController = controller; @@ -54,9 +54,9 @@ class AnimateCameraState extends State { children: [ Column( children: [ - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.newCameraPosition( const CameraPosition( bearing: 270.0, @@ -69,9 +69,9 @@ class AnimateCameraState extends State { }, child: const Text('newCameraPosition'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.newLatLng( const LatLng(56.1725505, 10.1850512), ), @@ -79,9 +79,9 @@ class AnimateCameraState extends State { }, child: const Text('newLatLng'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.newLatLngBounds( LatLngBounds( southwest: const LatLng(-38.483935, 113.248673), @@ -93,9 +93,9 @@ class AnimateCameraState extends State { }, child: const Text('newLatLngBounds'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.newLatLngZoom( const LatLng(37.4231613, -122.087159), 11.0, @@ -104,9 +104,9 @@ class AnimateCameraState extends State { }, child: const Text('newLatLngZoom'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.scrollBy(150.0, -225.0), ); }, @@ -116,9 +116,9 @@ class AnimateCameraState extends State { ), Column( children: [ - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.zoomBy( -0.5, const Offset(30.0, 20.0), @@ -127,33 +127,33 @@ class AnimateCameraState extends State { }, child: const Text('zoomBy with focus'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.zoomBy(-0.5), ); }, child: const Text('zoomBy'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.zoomIn(), ); }, child: const Text('zoomIn'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.zoomOut(), ); }, child: const Text('zoomOut'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.zoomTo(16.0), ); }, diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index 13763edb8216..b795040658e6 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart @@ -20,6 +20,7 @@ import 'place_polygon.dart'; import 'place_polyline.dart'; import 'scrolling_map.dart'; import 'snapshot.dart'; +import 'tile_overlay.dart'; final List _allPages = [ MapUiPage(), @@ -36,6 +37,7 @@ final List _allPages = [ PaddingPage(), SnapshotPage(), LiteModePage(), + TileOverlayPage(), ]; class MapsDemo extends StatelessWidget { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart index 029d3a1f187e..eaeb463e1b33 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart @@ -31,9 +31,9 @@ class _MapClickBody extends StatefulWidget { class _MapClickBodyState extends State<_MapClickBody> { _MapClickBodyState(); - GoogleMapController mapController; - LatLng _lastTap; - LatLng _lastLongPress; + GoogleMapController? mapController; + LatLng? _lastTap; + LatLng? _lastLongPress; @override Widget build(BuildContext context) { 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..20cccd6b61fd 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 @@ -31,7 +31,7 @@ class _MapCoordinatesBody extends StatefulWidget { class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { _MapCoordinatesBodyState(); - GoogleMapController mapController; + GoogleMapController? mapController; LatLngBounds _visibleRegion = LatLngBounds( southwest: const LatLng(0, 0), northeast: const LatLng(0, 0), @@ -83,11 +83,11 @@ 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 = - await mapController.getVisibleRegion(); + await mapController!.getVisibleRegion(); setState(() { _visibleRegion = 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..edae82e4074c 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 @@ -56,7 +56,7 @@ class MapUiBodyState extends State { bool _myLocationEnabled = true; bool _myTrafficEnabled = false; bool _myLocationButtonEnabled = true; - GoogleMapController _controller; + late GoogleMapController _controller; bool _nightMode = false; @override @@ -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(() { @@ -249,11 +249,10 @@ class MapUiBodyState extends State { }); } + // Should only be called if _isMapCreated is true. Widget _nightModeToggler() { - if (!_isMapCreated) { - return null; - } - return FlatButton( + assert(_isMapCreated); + return TextButton( child: Text('${_nightMode ? 'disable' : 'enable'} night mode'), onPressed: () { if (_nightMode) { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart index b62d898c3a3b..90404c347dd8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart @@ -29,8 +29,8 @@ class MarkerIconsBody extends StatefulWidget { const LatLng _kMapCenter = LatLng(52.4478, -3.5402); class MarkerIconsBodyState extends State { - GoogleMapController controller; - BitmapDescriptor _markerIcon; + GoogleMapController? controller; + BitmapDescriptor? _markerIcon; @override Widget build(BuildContext context) { @@ -48,7 +48,7 @@ class MarkerIconsBodyState extends State { target: _kMapCenter, zoom: 7.0, ), - markers: _createMarker(), + markers: {_createMarker()}, onMapCreated: _onMapCreated, ), ), @@ -57,17 +57,19 @@ class MarkerIconsBodyState extends State { ); } - Set _createMarker() { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return [ - Marker( + Marker _createMarker() { + if (_markerIcon != null) { + return Marker( markerId: MarkerId("marker_1"), position: _kMapCenter, - icon: _markerIcon, - ), - ].toSet(); + icon: _markerIcon!, + ); + } else { + return Marker( + markerId: MarkerId("marker_1"), + position: _kMapCenter, + ); + } } Future _createMarkerImageFromAsset(BuildContext context) async { 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..860b19e7925c 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 @@ -25,7 +25,7 @@ class MoveCamera extends StatefulWidget { } class MoveCameraState extends State { - GoogleMapController mapController; + GoogleMapController? mapController; void _onMapCreated(GoogleMapController controller) { mapController = controller; @@ -53,9 +53,9 @@ class MoveCameraState extends State { children: [ Column( children: [ - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.newCameraPosition( const CameraPosition( bearing: 270.0, @@ -68,9 +68,9 @@ class MoveCameraState extends State { }, child: const Text('newCameraPosition'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.newLatLng( const LatLng(56.1725505, 10.1850512), ), @@ -78,9 +78,9 @@ class MoveCameraState extends State { }, child: const Text('newLatLng'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.newLatLngBounds( LatLngBounds( southwest: const LatLng(-38.483935, 113.248673), @@ -92,9 +92,9 @@ class MoveCameraState extends State { }, child: const Text('newLatLngBounds'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.newLatLngZoom( const LatLng(37.4231613, -122.087159), 11.0, @@ -103,9 +103,9 @@ class MoveCameraState extends State { }, child: const Text('newLatLngZoom'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.scrollBy(150.0, -225.0), ); }, @@ -115,9 +115,9 @@ class MoveCameraState extends State { ), Column( children: [ - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.zoomBy( -0.5, const Offset(30.0, 20.0), @@ -126,33 +126,33 @@ class MoveCameraState extends State { }, child: const Text('zoomBy with focus'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.zoomBy(-0.5), ); }, child: const Text('zoomBy'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.zoomIn(), ); }, child: const Text('zoomIn'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.zoomOut(), ); }, child: const Text('zoomOut'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + 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..45b4a9e09e5c 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 @@ -27,7 +27,7 @@ class MarkerIconsBody extends StatefulWidget { const LatLng _kMapCenter = LatLng(52.4478, -3.5402); class MarkerIconsBodyState extends State { - GoogleMapController controller; + GoogleMapController? controller; EdgeInsets _padding = const EdgeInsets.all(0); @@ -147,19 +147,19 @@ class MarkerIconsBodyState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - FlatButton( + TextButton( child: const Text("Set Padding"), onPressed: () { setState(() { _padding = EdgeInsets.fromLTRB( - double.tryParse(_leftController.value?.text) ?? 0, - double.tryParse(_topController.value?.text) ?? 0, - double.tryParse(_rightController.value?.text) ?? 0, - double.tryParse(_bottomController.value?.text) ?? 0); + double.tryParse(_leftController.value.text) ?? 0, + double.tryParse(_topController.value.text) ?? 0, + double.tryParse(_rightController.value.text) ?? 0, + double.tryParse(_bottomController.value.text) ?? 0); }); }, ), - 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..59d30e51ce8b 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 @@ -28,10 +28,10 @@ class PlaceCircleBody extends StatefulWidget { class PlaceCircleBodyState extends State { PlaceCircleBodyState(); - GoogleMapController controller; + GoogleMapController? controller; Map circles = {}; int _circleIdCounter = 1; - CircleId selectedCircle; + CircleId? selectedCircle; // Values when toggling circle color int fillColorsIndex = 0; @@ -62,12 +62,14 @@ class PlaceCircleBodyState extends State { }); } - void _remove() { + void _remove(CircleId circleId) { setState(() { - if (circles.containsKey(selectedCircle)) { - circles.remove(selectedCircle); + if (circles.containsKey(circleId)) { + circles.remove(circleId); + } + if (circleId == selectedCircle) { + selectedCircle = null; } - selectedCircle = null; }); } @@ -100,37 +102,37 @@ class PlaceCircleBodyState extends State { }); } - void _toggleVisible() { - final Circle circle = circles[selectedCircle]; + void _toggleVisible(CircleId circleId) { + final Circle circle = circles[circleId]!; setState(() { - circles[selectedCircle] = circle.copyWith( + circles[circleId] = circle.copyWith( visibleParam: !circle.visible, ); }); } - void _changeFillColor() { - final Circle circle = circles[selectedCircle]; + void _changeFillColor(CircleId circleId) { + final Circle circle = circles[circleId]!; setState(() { - circles[selectedCircle] = circle.copyWith( + circles[circleId] = circle.copyWith( fillColorParam: colors[++fillColorsIndex % colors.length], ); }); } - void _changeStrokeColor() { - final Circle circle = circles[selectedCircle]; + void _changeStrokeColor(CircleId circleId) { + final Circle circle = circles[circleId]!; setState(() { - circles[selectedCircle] = circle.copyWith( + circles[circleId] = circle.copyWith( strokeColorParam: colors[++strokeColorsIndex % colors.length], ); }); } - void _changeStrokeWidth() { - final Circle circle = circles[selectedCircle]; + void _changeStrokeWidth(CircleId circleId) { + final Circle circle = circles[circleId]!; setState(() { - circles[selectedCircle] = circle.copyWith( + circles[circleId] = circle.copyWith( strokeWidthParam: widths[++widthsIndex % widths.length], ); }); @@ -138,6 +140,7 @@ class PlaceCircleBodyState extends State { @override Widget build(BuildContext context) { + final CircleId? selectedId = selectedCircle; return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -165,40 +168,43 @@ 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, + onPressed: (selectedId == null) + ? null + : () => _remove(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle visible'), - onPressed: - (selectedCircle == null) ? null : _toggleVisible, + onPressed: (selectedId == null) + ? null + : () => _toggleVisible(selectedId), ), ], ), Column( children: [ - FlatButton( + TextButton( child: const Text('change stroke width'), - onPressed: (selectedCircle == null) + onPressed: (selectedId == null) ? null - : _changeStrokeWidth, + : () => _changeStrokeWidth(selectedId), ), - FlatButton( + TextButton( child: const Text('change stroke color'), - onPressed: (selectedCircle == null) + onPressed: (selectedId == null) ? null - : _changeStrokeColor, + : () => _changeStrokeColor(selectedId), ), - FlatButton( + TextButton( child: const Text('change fill color'), - onPressed: (selectedCircle == null) + onPressed: (selectedId == null) ? null - : _changeFillColor, + : () => _changeFillColor(selectedId), ), ], ) 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..c650ca34c1b0 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 @@ -35,9 +35,9 @@ class PlaceMarkerBodyState extends State { PlaceMarkerBodyState(); static final LatLng center = const LatLng(-33.86711, 151.1947171); - GoogleMapController controller; + GoogleMapController? controller; Map markers = {}; - MarkerId selectedMarker; + MarkerId? selectedMarker; int _markerIdCounter = 1; void _onMapCreated(GoogleMapController controller) { @@ -50,13 +50,14 @@ class PlaceMarkerBodyState extends State { } void _onMarkerTapped(MarkerId markerId) { - final Marker tappedMarker = markers[markerId]; + final Marker? tappedMarker = markers[markerId]; if (tappedMarker != null) { setState(() { - if (markers.containsKey(selectedMarker)) { - final Marker resetOld = markers[selectedMarker] + final MarkerId? previousMarkerId = selectedMarker; + if (previousMarkerId != null && markers.containsKey(previousMarkerId)) { + final Marker resetOld = markers[previousMarkerId]! .copyWith(iconParam: BitmapDescriptor.defaultMarker); - markers[selectedMarker] = resetOld; + markers[previousMarkerId] = resetOld; } selectedMarker = markerId; final Marker newMarker = tappedMarker.copyWith( @@ -70,14 +71,14 @@ class PlaceMarkerBodyState extends State { } void _onMarkerDragEnd(MarkerId markerId, LatLng newPosition) async { - final Marker tappedMarker = markers[markerId]; + final Marker? tappedMarker = markers[markerId]; if (tappedMarker != null) { await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( actions: [ - FlatButton( + TextButton( child: const Text('OK'), onPressed: () => Navigator.of(context).pop(), ) @@ -126,23 +127,23 @@ class PlaceMarkerBodyState extends State { }); } - void _remove() { + void _remove(MarkerId markerId) { setState(() { - if (markers.containsKey(selectedMarker)) { - markers.remove(selectedMarker); + if (markers.containsKey(markerId)) { + markers.remove(markerId); } }); } - void _changePosition() { - final Marker marker = markers[selectedMarker]; + void _changePosition(MarkerId markerId) { + final Marker marker = markers[markerId]!; final LatLng current = marker.position; final Offset offset = Offset( center.latitude - current.latitude, center.longitude - current.longitude, ); setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( positionParam: LatLng( center.latitude + offset.dy, center.longitude + offset.dx, @@ -151,23 +152,23 @@ class PlaceMarkerBodyState extends State { }); } - void _changeAnchor() { - final Marker marker = markers[selectedMarker]; + void _changeAnchor(MarkerId markerId) { + final Marker marker = markers[markerId]!; final Offset currentAnchor = marker.anchor; final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( anchorParam: newAnchor, ); }); } - Future _changeInfoAnchor() async { - final Marker marker = markers[selectedMarker]; + Future _changeInfoAnchor(MarkerId markerId) async { + final Marker marker = markers[markerId]!; final Offset currentAnchor = marker.infoWindow.anchor; final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( infoWindowParam: marker.infoWindow.copyWith( anchorParam: newAnchor, ), @@ -175,29 +176,29 @@ class PlaceMarkerBodyState extends State { }); } - Future _toggleDraggable() async { - final Marker marker = markers[selectedMarker]; + Future _toggleDraggable(MarkerId markerId) async { + final Marker marker = markers[markerId]!; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( draggableParam: !marker.draggable, ); }); } - Future _toggleFlat() async { - final Marker marker = markers[selectedMarker]; + Future _toggleFlat(MarkerId markerId) async { + final Marker marker = markers[markerId]!; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( flatParam: !marker.flat, ); }); } - Future _changeInfo() async { - final Marker marker = markers[selectedMarker]; - final String newSnippet = marker.infoWindow.snippet + '*'; + Future _changeInfo(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + final String newSnippet = marker.infoWindow.snippet! + '*'; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( infoWindowParam: marker.infoWindow.copyWith( snippetParam: newSnippet, ), @@ -205,40 +206,40 @@ class PlaceMarkerBodyState extends State { }); } - Future _changeAlpha() async { - final Marker marker = markers[selectedMarker]; + Future _changeAlpha(MarkerId markerId) async { + final Marker marker = markers[markerId]!; final double current = marker.alpha; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( alphaParam: current < 0.1 ? 1.0 : current * 0.75, ); }); } - Future _changeRotation() async { - final Marker marker = markers[selectedMarker]; + Future _changeRotation(MarkerId markerId) async { + final Marker marker = markers[markerId]!; final double current = marker.rotation; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( rotationParam: current == 330.0 ? 0.0 : current + 30.0, ); }); } - Future _toggleVisible() async { - final Marker marker = markers[selectedMarker]; + Future _toggleVisible(MarkerId markerId) async { + final Marker marker = markers[markerId]!; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( visibleParam: !marker.visible, ); }); } - Future _changeZIndex() async { - final Marker marker = markers[selectedMarker]; + Future _changeZIndex(MarkerId markerId) async { + final Marker marker = markers[markerId]!; final double current = marker.zIndex; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( zIndexParam: current == 12.0 ? 0.0 : current + 1.0, ); }); @@ -283,6 +284,7 @@ class PlaceMarkerBodyState extends State { @override Widget build(BuildContext context) { + final MarkerId? selectedId = selectedMarker; return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -297,9 +299,6 @@ class PlaceMarkerBodyState extends State { target: LatLng(-33.852, 151.211), zoom: 11.0, ), - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals markers: Set.of(markers.values), ), ), @@ -313,57 +312,79 @@ class PlaceMarkerBodyState extends State { children: [ Column( children: [ - FlatButton( + TextButton( child: const Text('add'), onPressed: _add, ), - FlatButton( + TextButton( child: const Text('remove'), - onPressed: _remove, + onPressed: selectedId == null + ? null + : () => _remove(selectedId), ), - FlatButton( + TextButton( child: const Text('change info'), - onPressed: _changeInfo, + onPressed: selectedId == null + ? null + : () => _changeInfo(selectedId), ), - FlatButton( + TextButton( child: const Text('change info anchor'), - onPressed: _changeInfoAnchor, + onPressed: selectedId == null + ? null + : () => _changeInfoAnchor(selectedId), ), ], ), Column( children: [ - FlatButton( + TextButton( child: const Text('change alpha'), - onPressed: _changeAlpha, + onPressed: selectedId == null + ? null + : () => _changeAlpha(selectedId), ), - FlatButton( + TextButton( child: const Text('change anchor'), - onPressed: _changeAnchor, + onPressed: selectedId == null + ? null + : () => _changeAnchor(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle draggable'), - onPressed: _toggleDraggable, + onPressed: selectedId == null + ? null + : () => _toggleDraggable(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle flat'), - onPressed: _toggleFlat, + onPressed: selectedId == null + ? null + : () => _toggleFlat(selectedId), ), - FlatButton( + TextButton( child: const Text('change position'), - onPressed: _changePosition, + onPressed: selectedId == null + ? null + : () => _changePosition(selectedId), ), - FlatButton( + TextButton( child: const Text('change rotation'), - onPressed: _changeRotation, + onPressed: selectedId == null + ? null + : () => _changeRotation(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle visible'), - onPressed: _toggleVisible, + onPressed: selectedId == null + ? null + : () => _toggleVisible(selectedId), ), - FlatButton( + TextButton( child: const Text('change zIndex'), - onPressed: _changeZIndex, + onPressed: selectedId == null + ? null + : () => _changeZIndex(selectedId), ), // A breaking change to the ImageStreamListener API affects this sample. // I've updates the sample to use the new API, but as we cannot use the new @@ -371,7 +392,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..8ee58420f508 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 @@ -28,10 +28,11 @@ class PlacePolygonBody extends StatefulWidget { class PlacePolygonBodyState extends State { PlacePolygonBodyState(); - GoogleMapController controller; + GoogleMapController? controller; Map polygons = {}; - int _polygonIdCounter = 1; - PolygonId selectedPolygon; + Map polygonOffsets = {}; + int _polygonIdCounter = 0; + PolygonId? selectedPolygon; // Values when toggling polygon color int strokeColorsIndex = 0; @@ -62,10 +63,10 @@ class PlacePolygonBodyState extends State { }); } - void _remove() { + void _remove(PolygonId polygonId) { setState(() { - if (polygons.containsKey(selectedPolygon)) { - polygons.remove(selectedPolygon); + if (polygons.containsKey(polygonId)) { + polygons.remove(polygonId); } selectedPolygon = null; }); @@ -79,7 +80,6 @@ class PlacePolygonBodyState extends State { } final String polygonIdVal = 'polygon_id_$_polygonIdCounter'; - _polygonIdCounter++; final PolygonId polygonId = PolygonId(polygonIdVal); final Polygon polygon = Polygon( @@ -96,56 +96,77 @@ class PlacePolygonBodyState extends State { setState(() { polygons[polygonId] = polygon; + polygonOffsets[polygonId] = _polygonIdCounter.ceilToDouble(); + // increment _polygonIdCounter to have unique polygon id each time + _polygonIdCounter++; }); } - void _toggleGeodesic() { - final Polygon polygon = polygons[selectedPolygon]; + void _toggleGeodesic(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; setState(() { - polygons[selectedPolygon] = polygon.copyWith( + polygons[polygonId] = polygon.copyWith( geodesicParam: !polygon.geodesic, ); }); } - void _toggleVisible() { - final Polygon polygon = polygons[selectedPolygon]; + void _toggleVisible(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; setState(() { - polygons[selectedPolygon] = polygon.copyWith( + polygons[polygonId] = polygon.copyWith( visibleParam: !polygon.visible, ); }); } - void _changeStrokeColor() { - final Polygon polygon = polygons[selectedPolygon]; + void _changeStrokeColor(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; setState(() { - polygons[selectedPolygon] = polygon.copyWith( + polygons[polygonId] = polygon.copyWith( strokeColorParam: colors[++strokeColorsIndex % colors.length], ); }); } - void _changeFillColor() { - final Polygon polygon = polygons[selectedPolygon]; + void _changeFillColor(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; setState(() { - polygons[selectedPolygon] = polygon.copyWith( + polygons[polygonId] = polygon.copyWith( fillColorParam: colors[++fillColorsIndex % colors.length], ); }); } - void _changeWidth() { - final Polygon polygon = polygons[selectedPolygon]; + void _changeWidth(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; setState(() { - polygons[selectedPolygon] = polygon.copyWith( + polygons[polygonId] = polygon.copyWith( strokeWidthParam: widths[++widthsIndex % widths.length], ); }); } + void _addHoles(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = + polygon.copyWith(holesParam: _createHoles(polygonId)); + }); + } + + void _removeHoles(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + holesParam: >[], + ); + }); + } + @override Widget build(BuildContext context) { + final PolygonId? selectedId = selectedPolygon; return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -173,45 +194,65 @@ 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, + onPressed: (selectedId == null) + ? null + : () => _remove(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle visible'), - onPressed: - (selectedPolygon == null) ? null : _toggleVisible, + onPressed: (selectedId == null) + ? null + : () => _toggleVisible(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle geodesic'), - onPressed: (selectedPolygon == null) + onPressed: (selectedId == null) ? null - : _toggleGeodesic, + : () => _toggleGeodesic(selectedId), ), ], ), Column( children: [ - FlatButton( + TextButton( + child: const Text('add holes'), + onPressed: (selectedId == null) + ? null + : ((polygons[selectedId]!.holes.isNotEmpty) + ? null + : () => _addHoles(selectedId)), + ), + TextButton( + child: const Text('remove holes'), + onPressed: (selectedId == null) + ? null + : ((polygons[selectedId]!.holes.isEmpty) + ? null + : () => _removeHoles(selectedId)), + ), + TextButton( child: const Text('change stroke width'), - onPressed: - (selectedPolygon == null) ? null : _changeWidth, + onPressed: (selectedId == null) + ? null + : () => _changeWidth(selectedId), ), - FlatButton( + TextButton( child: const Text('change stroke color'), - onPressed: (selectedPolygon == null) + onPressed: (selectedId == null) ? null - : _changeStrokeColor, + : () => _changeStrokeColor(selectedId), ), - FlatButton( + TextButton( child: const Text('change fill color'), - onPressed: (selectedPolygon == null) + onPressed: (selectedId == null) ? null - : _changeFillColor, + : () => _changeFillColor(selectedId), ), ], ) @@ -235,6 +276,27 @@ class PlacePolygonBodyState extends State { return points; } + List> _createHoles(PolygonId polygonId) { + final List> holes = >[]; + final double offset = polygonOffsets[polygonId]!; + + final List hole1 = []; + hole1.add(_createLatLng(51.8395 + offset, -3.8814)); + hole1.add(_createLatLng(52.0234 + offset, -3.9914)); + hole1.add(_createLatLng(52.1351 + offset, -4.4435)); + hole1.add(_createLatLng(52.0231 + offset, -4.5829)); + holes.add(hole1); + + final List hole2 = []; + hole2.add(_createLatLng(52.2395 + offset, -3.6814)); + hole2.add(_createLatLng(52.4234 + offset, -3.7914)); + hole2.add(_createLatLng(52.5351 + offset, -4.2435)); + hole2.add(_createLatLng(52.4231 + offset, -4.3829)); + holes.add(hole2); + + return holes; + } + LatLng _createLatLng(double lat, double lng) { return LatLng(lat, lng); } 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..c66343d78f00 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 @@ -29,10 +29,10 @@ class PlacePolylineBody extends StatefulWidget { class PlacePolylineBodyState extends State { PlacePolylineBodyState(); - GoogleMapController controller; + GoogleMapController? controller; Map polylines = {}; - int _polylineIdCounter = 1; - PolylineId selectedPolyline; + int _polylineIdCounter = 0; + PolylineId? selectedPolyline; // Values when toggling polyline color int colorsIndex = 0; @@ -91,10 +91,10 @@ class PlacePolylineBodyState extends State { }); } - void _remove() { + void _remove(PolylineId polylineId) { setState(() { - if (polylines.containsKey(selectedPolyline)) { - polylines.remove(selectedPolyline); + if (polylines.containsKey(polylineId)) { + polylines.remove(polylineId); } selectedPolyline = null; }); @@ -127,73 +127,73 @@ class PlacePolylineBodyState extends State { }); } - void _toggleGeodesic() { - final Polyline polyline = polylines[selectedPolyline]; + void _toggleGeodesic(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( geodesicParam: !polyline.geodesic, ); }); } - void _toggleVisible() { - final Polyline polyline = polylines[selectedPolyline]; + void _toggleVisible(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( visibleParam: !polyline.visible, ); }); } - void _changeColor() { - final Polyline polyline = polylines[selectedPolyline]; + void _changeColor(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( colorParam: colors[++colorsIndex % colors.length], ); }); } - void _changeWidth() { - final Polyline polyline = polylines[selectedPolyline]; + void _changeWidth(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( widthParam: widths[++widthsIndex % widths.length], ); }); } - void _changeJointType() { - final Polyline polyline = polylines[selectedPolyline]; + void _changeJointType(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( jointTypeParam: jointTypes[++jointTypesIndex % jointTypes.length], ); }); } - void _changeEndCap() { - final Polyline polyline = polylines[selectedPolyline]; + void _changeEndCap(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( endCapParam: endCaps[++endCapsIndex % endCaps.length], ); }); } - void _changeStartCap() { - final Polyline polyline = polylines[selectedPolyline]; + void _changeStartCap(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( startCapParam: startCaps[++startCapsIndex % startCaps.length], ); }); } - void _changePattern() { - final Polyline polyline = polylines[selectedPolyline]; + void _changePattern(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( patternsParam: patterns[++patternsIndex % patterns.length], ); }); @@ -201,9 +201,9 @@ class PlacePolylineBodyState extends State { @override Widget build(BuildContext context) { - final bool iOSorNotSelected = - (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) || - (selectedPolyline == null); + final bool isIOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; + + final PolylineId? selectedId = selectedPolyline; return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, @@ -215,7 +215,7 @@ class PlacePolylineBodyState extends State { height: 300.0, child: GoogleMap( initialCameraPosition: const CameraPosition( - target: LatLng(52.4478, -3.5402), + target: LatLng(53.1721, -3.5402), zoom: 7.0, ), polylines: Set.of(polylines.values), @@ -232,56 +232,67 @@ 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, + onPressed: (selectedId == null) + ? null + : () => _remove(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle visible'), - onPressed: (selectedPolyline == null) + onPressed: (selectedId == null) ? null - : _toggleVisible, + : () => _toggleVisible(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle geodesic'), - onPressed: (selectedPolyline == null) + onPressed: (selectedId == null) ? null - : _toggleGeodesic, + : () => _toggleGeodesic(selectedId), ), ], ), Column( children: [ - FlatButton( + TextButton( child: const Text('change width'), - onPressed: - (selectedPolyline == null) ? null : _changeWidth, + onPressed: (selectedId == null) + ? null + : () => _changeWidth(selectedId), ), - FlatButton( + TextButton( child: const Text('change color'), - onPressed: - (selectedPolyline == null) ? null : _changeColor, + onPressed: (selectedId == null) + ? null + : () => _changeColor(selectedId), ), - FlatButton( + TextButton( child: const Text('change start cap [Android only]'), - onPressed: iOSorNotSelected ? null : _changeStartCap, + onPressed: isIOS || (selectedId == null) + ? null + : () => _changeStartCap(selectedId), ), - FlatButton( + TextButton( child: const Text('change end cap [Android only]'), - onPressed: iOSorNotSelected ? null : _changeEndCap, + onPressed: isIOS || (selectedId == null) + ? null + : () => _changeEndCap(selectedId), ), - FlatButton( + TextButton( child: const Text('change joint type [Android only]'), - onPressed: iOSorNotSelected ? null : _changeJointType, + onPressed: isIOS || (selectedId == null) + ? null + : () => _changeJointType(selectedId), ), - FlatButton( + TextButton( child: const Text('change pattern [Android only]'), - onPressed: iOSorNotSelected ? null : _changePattern, + onPressed: isIOS || (selectedId == null) + ? null + : () => _changePattern(selectedId), ), ], ) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart index 2aa1243fd27c..b7d88bc46a58 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart @@ -48,15 +48,12 @@ class ScrollingMapBody extends StatelessWidget { target: center, zoom: 11.0, ), - gestureRecognizers: - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - >[ + gestureRecognizers: // + >{ Factory( () => EagerGestureRecognizer(), ), - ].toSet(), + }, ), ), ), @@ -84,34 +81,25 @@ class ScrollingMapBody extends StatelessWidget { target: center, zoom: 11.0, ), - markers: - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - Set.of( - [ - Marker( - markerId: MarkerId("test_marker_id"), - position: LatLng( - center.latitude, - center.longitude, - ), - infoWindow: const InfoWindow( - title: 'An interesting location', - snippet: '*', - ), - ) - ], - ), - gestureRecognizers: - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - >[ + markers: { + Marker( + markerId: MarkerId("test_marker_id"), + position: LatLng( + center.latitude, + center.longitude, + ), + infoWindow: const InfoWindow( + title: 'An interesting location', + snippet: '*', + ), + ), + }, + gestureRecognizers: < + Factory>{ Factory( () => ScaleGestureRecognizer(), ), - ].toSet(), + }, ), ), ), 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..3ba12bfac3f2 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 @@ -30,8 +30,8 @@ class _SnapshotBody extends StatefulWidget { } class _SnapshotBodyState extends State<_SnapshotBody> { - GoogleMapController _mapController; - Uint8List _imageBytes; + GoogleMapController? _mapController; + Uint8List? _imageBytes; @override Widget build(BuildContext context) { @@ -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(); @@ -59,7 +59,7 @@ class _SnapshotBodyState extends State<_SnapshotBody> { Container( decoration: BoxDecoration(color: Colors.blueGrey[50]), height: 180, - child: _imageBytes != null ? Image.memory(_imageBytes) : null, + child: _imageBytes != null ? Image.memory(_imageBytes!) : null, ), ], ), diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart new file mode 100644 index 000000000000..8ae3b3bca979 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart @@ -0,0 +1,153 @@ +// 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. + +// ignore_for_file: public_member_api_docs + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'page.dart'; + +class TileOverlayPage extends GoogleMapExampleAppPage { + TileOverlayPage() : super(const Icon(Icons.map), 'Tile overlay'); + + @override + Widget build(BuildContext context) { + return const TileOverlayBody(); + } +} + +class TileOverlayBody extends StatefulWidget { + const TileOverlayBody(); + + @override + State createState() => TileOverlayBodyState(); +} + +class TileOverlayBodyState extends State { + TileOverlayBodyState(); + + GoogleMapController? controller; + TileOverlay? _tileOverlay; + + void _onMapCreated(GoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _removeTileOverlay() { + setState(() { + _tileOverlay = null; + }); + } + + void _addTileOverlay() { + final TileOverlay tileOverlay = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + ); + setState(() { + _tileOverlay = tileOverlay; + }); + } + + void _clearTileCache() { + if (_tileOverlay != null && controller != null) { + controller!.clearTileCache(_tileOverlay!.tileOverlayId); + } + } + + @override + Widget build(BuildContext context) { + Set overlays = { + if (_tileOverlay != null) _tileOverlay, + }; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: GoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(59.935460, 30.325177), + zoom: 7.0, + ), + tileOverlays: overlays as Set, + onMapCreated: _onMapCreated, + ), + ), + ), + TextButton( + child: const Text('Add tile overlay'), + onPressed: _addTileOverlay, + ), + TextButton( + child: const Text('Remove tile overlay'), + onPressed: _removeTileOverlay, + ), + TextButton( + child: const Text('Clear tile cache'), + onPressed: _clearTileCache, + ), + ], + ); + } +} + +class _DebugTileProvider implements TileProvider { + _DebugTileProvider() { + boxPaint.isAntiAlias = true; + boxPaint.color = Colors.blue; + boxPaint.strokeWidth = 2.0; + boxPaint.style = PaintingStyle.stroke; + } + + static const int width = 100; + static const int height = 100; + static final Paint boxPaint = Paint(); + static final TextStyle textStyle = TextStyle( + color: Colors.red, + fontSize: 20, + ); + + @override + Future getTile(int x, int y, int? zoom) async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final TextSpan textSpan = TextSpan( + text: '$x,$y', + style: textStyle, + ); + final TextPainter textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + textPainter.layout( + minWidth: 0.0, + maxWidth: width.toDouble(), + ); + final Offset offset = const Offset(0, 0); + textPainter.paint(canvas, offset); + canvas.drawRect( + Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); + final ui.Picture picture = recorder.endRecording(); + final Uint8List byteData = await picture + .toImage(width, height) + .then((ui.Image image) => + image.toByteData(format: ui.ImageByteFormat.png)) + .then((ByteData? byteData) => byteData!.buffer.asUint8List()); + return Tile(width, height, byteData); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index e0f79b571cb0..35d0da3488be 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -2,65 +2,31 @@ name: google_maps_flutter_example description: Demonstrates how to use the google_maps_flutter plugin. environment: - sdk: ">=2.2.0 <3.0.0" - flutter: ">=1.22.0 <2.0.0" + sdk: '>=2.12.0-259.9.beta <3.0.0' + flutter: ">=1.22.0" dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.0 google_maps_flutter: + # When depending on this package from a real application you should use: + # google_maps_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - flutter_plugin_android_lifecycle: ^1.0.0 + flutter_plugin_android_lifecycle: ^2.0.0-nullsafety.2 dev_dependencies: flutter_driver: sdk: flutter integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 -# For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: assets: - assets/ - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.io/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.io/assets-and-images/#from-packages - - # To add custom fonts to your application, 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 - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.io/custom-fonts/#from-packages diff --git a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/integration_test.dart b/packages/google_maps_flutter/google_maps_flutter/example/test_driver/integration_test.dart index 7a2c21338786..ceb6c4d6a3a0 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/integration_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/test_driver/integration_test.dart @@ -1,6 +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. +// @dart=2.9 import 'dart:async'; import 'dart:convert'; diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h new file mode 100644 index 000000000000..f84ad7c20bb4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h @@ -0,0 +1,42 @@ +// 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 +#import + +NS_ASSUME_NONNULL_BEGIN + +// Defines map UI options writable from Flutter. +@protocol FLTGoogleMapTileOverlayOptionsSink +- (void)setFadeIn:(BOOL)fadeIn; +- (void)setTransparency:(float)transparency; +- (void)setZIndex:(int)zIndex; +- (void)setVisible:(BOOL)visible; +- (void)setTileSize:(NSInteger)tileSize; +@end + +@interface FLTGoogleMapTileOverlayController : NSObject +- (instancetype)initWithTileLayer:(GMSTileLayer *)tileLayer mapView:(GMSMapView *)mapView; +- (void)removeTileOverlay; +- (void)clearTileCache; +- (NSDictionary *)getTileOverlayInfo; +@end + +@interface FLTTileProviderController : GMSTileLayer +@property(copy, nonatomic, readonly) NSString *tileOverlayId; +- (instancetype)init:(FlutterMethodChannel *)methodChannel tileOverlayId:(NSString *)tileOverlayId; +@end + +@interface FLTTileOverlaysController : NSObject +- (instancetype)init:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar; +- (void)addTileOverlays:(NSArray *)tileOverlaysToAdd; +- (void)changeTileOverlays:(NSArray *)tileOverlaysToChange; +- (void)removeTileOverlayIds:(NSArray *)tileOverlayIdsToRemove; +- (void)clearTileCache:(NSString *)tileOverlayId; +- (nullable NSDictionary *)getTileOverlayInfo:(NSString *)tileverlayId; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m new file mode 100644 index 000000000000..7fbd7c53b90a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m @@ -0,0 +1,234 @@ +// 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 "FLTGoogleMapTileOverlayController.h" +#import "JsonConversions.h" + +static void InterpretTileOverlayOptions(NSDictionary* data, + id sink, + NSObject* registrar) { + NSNumber* visible = data[@"visible"]; + if (visible != nil) { + [sink setVisible:visible.boolValue]; + } + + NSNumber* transparency = data[@"transparency"]; + if (transparency != nil) { + [sink setTransparency:transparency.floatValue]; + } + + NSNumber* zIndex = data[@"zIndex"]; + if (zIndex != nil) { + [sink setZIndex:zIndex.intValue]; + } + + NSNumber* fadeIn = data[@"fadeIn"]; + if (fadeIn != nil) { + [sink setFadeIn:fadeIn.boolValue]; + } + + NSNumber* tileSize = data[@"tileSize"]; + if (tileSize != nil) { + [sink setTileSize:tileSize.integerValue]; + } +} + +@interface FLTGoogleMapTileOverlayController () + +@property(strong, nonatomic) GMSTileLayer* layer; +@property(weak, nonatomic) GMSMapView* mapView; + +@end + +@implementation FLTGoogleMapTileOverlayController + +- (instancetype)initWithTileLayer:(GMSTileLayer*)tileLayer mapView:(GMSMapView*)mapView { + self = [super init]; + if (self) { + self.layer = tileLayer; + self.mapView = mapView; + } + return self; +} + +- (void)removeTileOverlay { + self.layer.map = nil; +} + +- (void)clearTileCache { + [self.layer clearTileCache]; +} + +- (NSDictionary*)getTileOverlayInfo { + NSMutableDictionary* info = [[NSMutableDictionary alloc] init]; + BOOL visible = self.layer.map != nil; + info[@"visible"] = @(visible); + info[@"fadeIn"] = @(self.layer.fadeIn); + float transparency = 1.0 - self.layer.opacity; + info[@"transparency"] = @(transparency); + info[@"zIndex"] = @(self.layer.zIndex); + return info; +} + +#pragma mark - FLTGoogleMapTileOverlayOptionsSink methods + +- (void)setFadeIn:(BOOL)fadeIn { + self.layer.fadeIn = fadeIn; +} + +- (void)setTransparency:(float)transparency { + float opacity = 1.0 - transparency; + self.layer.opacity = opacity; +} + +- (void)setVisible:(BOOL)visible { + self.layer.map = visible ? self.mapView : nil; +} + +- (void)setZIndex:(int)zIndex { + self.layer.zIndex = zIndex; +} + +- (void)setTileSize:(NSInteger)tileSize { + self.layer.tileSize = tileSize; +} +@end + +@interface FLTTileProviderController () + +@property(weak, nonatomic) FlutterMethodChannel* methodChannel; +@property(copy, nonatomic, readwrite) NSString* tileOverlayId; + +@end + +@implementation FLTTileProviderController + +- (instancetype)init:(FlutterMethodChannel*)methodChannel tileOverlayId:(NSString*)tileOverlayId { + self = [super init]; + if (self) { + self.methodChannel = methodChannel; + self.tileOverlayId = tileOverlayId; + } + return self; +} + +#pragma mark - GMSTileLayer method + +- (void)requestTileForX:(NSUInteger)x + y:(NSUInteger)y + zoom:(NSUInteger)zoom + receiver:(id)receiver { + [self.methodChannel + invokeMethod:@"tileOverlay#getTile" + arguments:@{ + @"tileOverlayId" : self.tileOverlayId, + @"x" : @(x), + @"y" : @(y), + @"zoom" : @(zoom) + } + result:^(id _Nullable result) { + UIImage* tileImage; + if ([result isKindOfClass:[NSDictionary class]]) { + FlutterStandardTypedData* typedData = (FlutterStandardTypedData*)result[@"data"]; + if (typedData == nil) { + tileImage = kGMSTileLayerNoTile; + } else { + tileImage = [UIImage imageWithData:typedData.data]; + } + } else { + if ([result isKindOfClass:[FlutterError class]]) { + FlutterError* error = (FlutterError*)result; + NSLog(@"Can't get tile: errorCode = %@, errorMessage = %@, details = %@", + [error code], [error message], [error details]); + } + if ([result isKindOfClass:[FlutterMethodNotImplemented class]]) { + NSLog(@"Can't get tile: notImplemented"); + } + tileImage = kGMSTileLayerNoTile; + } + + [receiver receiveTileWithX:x y:y zoom:zoom image:tileImage]; + }]; +} + +@end + +@interface FLTTileOverlaysController () + +@property(strong, nonatomic) NSMutableDictionary* tileOverlayIdToController; +@property(weak, nonatomic) FlutterMethodChannel* methodChannel; +@property(weak, nonatomic) NSObject* registrar; +@property(weak, nonatomic) GMSMapView* mapView; + +@end + +@implementation FLTTileOverlaysController + +- (instancetype)init:(FlutterMethodChannel*)methodChannel + mapView:(GMSMapView*)mapView + registrar:(NSObject*)registrar { + self = [super init]; + if (self) { + self.methodChannel = methodChannel; + self.mapView = mapView; + self.tileOverlayIdToController = [[NSMutableDictionary alloc] init]; + self.registrar = registrar; + } + return self; +} + +- (void)addTileOverlays:(NSArray*)tileOverlaysToAdd { + for (NSDictionary* tileOverlay in tileOverlaysToAdd) { + NSString* tileOverlayId = [FLTTileOverlaysController getTileOverlayId:tileOverlay]; + FLTTileProviderController* tileProvider = + [[FLTTileProviderController alloc] init:self.methodChannel tileOverlayId:tileOverlayId]; + FLTGoogleMapTileOverlayController* controller = + [[FLTGoogleMapTileOverlayController alloc] initWithTileLayer:tileProvider + mapView:self.mapView]; + InterpretTileOverlayOptions(tileOverlay, controller, self.registrar); + self.tileOverlayIdToController[tileOverlayId] = controller; + } +} + +- (void)changeTileOverlays:(NSArray*)tileOverlaysToChange { + for (NSDictionary* tileOverlay in tileOverlaysToChange) { + NSString* tileOverlayId = [FLTTileOverlaysController getTileOverlayId:tileOverlay]; + FLTGoogleMapTileOverlayController* controller = self.tileOverlayIdToController[tileOverlayId]; + if (!controller) { + continue; + } + InterpretTileOverlayOptions(tileOverlay, controller, self.registrar); + } +} +- (void)removeTileOverlayIds:(NSArray*)tileOverlayIdsToRemove { + for (NSString* tileOverlayId in tileOverlayIdsToRemove) { + FLTGoogleMapTileOverlayController* controller = self.tileOverlayIdToController[tileOverlayId]; + if (!controller) { + continue; + } + [controller removeTileOverlay]; + [self.tileOverlayIdToController removeObjectForKey:tileOverlayId]; + } +} + +- (void)clearTileCache:(NSString*)tileOverlayId { + FLTGoogleMapTileOverlayController* controller = self.tileOverlayIdToController[tileOverlayId]; + if (!controller) { + return; + } + [controller clearTileCache]; +} + +- (nullable NSDictionary*)getTileOverlayInfo:(NSString*)tileverlayId { + if (self.tileOverlayIdToController[tileverlayId] == nil) { + return nil; + } + return [self.tileOverlayIdToController[tileverlayId] getTileOverlayInfo]; +} + ++ (NSString*)getTileOverlayId:(NSDictionary*)tileOverlay { + return tileOverlay[@"tileOverlayId"]; +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m index 321ddd318536..749ce9e84a4e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m @@ -3,6 +3,7 @@ // found in the LICENSE file. #import "GoogleMapController.h" +#import "FLTGoogleMapTileOverlayController.h" #import "JsonConversions.h" #pragma mark - Conversion of JSON-like values sent via platform channels. Forward declarations. @@ -55,6 +56,7 @@ @implementation FLTGoogleMapController { FLTPolygonsController* _polygonsController; FLTPolylinesController* _polylinesController; FLTCirclesController* _circlesController; + FLTTileOverlaysController* _tileOverlaysController; } - (instancetype)initWithFrame:(CGRect)frame @@ -94,6 +96,9 @@ - (instancetype)initWithFrame:(CGRect)frame _circlesController = [[FLTCirclesController alloc] init:_channel mapView:_mapView registrar:registrar]; + _tileOverlaysController = [[FLTTileOverlaysController alloc] init:_channel + mapView:_mapView + registrar:registrar]; id markersToAdd = args[@"markersToAdd"]; if ([markersToAdd isKindOfClass:[NSArray class]]) { [_markersController addMarkers:markersToAdd]; @@ -110,6 +115,10 @@ - (instancetype)initWithFrame:(CGRect)frame if ([circlesToAdd isKindOfClass:[NSArray class]]) { [_circlesController addCircles:circlesToAdd]; } + id tileOverlaysToAdd = args[@"tileOverlaysToAdd"]; + if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) { + [_tileOverlaysController addTileOverlays:tileOverlaysToAdd]; + } } return self; } @@ -298,6 +307,24 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [_circlesController removeCircleIds:circleIdsToRemove]; } result(nil); + } else if ([call.method isEqualToString:@"tileOverlays#update"]) { + id tileOverlaysToAdd = call.arguments[@"tileOverlaysToAdd"]; + if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) { + [_tileOverlaysController addTileOverlays:tileOverlaysToAdd]; + } + id tileOverlaysToChange = call.arguments[@"tileOverlaysToChange"]; + if ([tileOverlaysToChange isKindOfClass:[NSArray class]]) { + [_tileOverlaysController changeTileOverlays:tileOverlaysToChange]; + } + id tileOverlayIdsToRemove = call.arguments[@"tileOverlayIdsToRemove"]; + if ([tileOverlayIdsToRemove isKindOfClass:[NSArray class]]) { + [_tileOverlaysController removeTileOverlayIds:tileOverlayIdsToRemove]; + } + result(nil); + } else if ([call.method isEqualToString:@"tileOverlays#clearTileCache"]) { + id rawTileOverlayId = call.arguments[@"tileOverlayId"]; + [_tileOverlaysController clearTileCache:rawTileOverlayId]; + result(nil); } else if ([call.method isEqualToString:@"map#isCompassEnabled"]) { NSNumber* isCompassEnabled = @(_mapView.settings.compassButton); result(isCompassEnabled); @@ -341,6 +368,9 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else { result(@[ @(NO), error ]); } + } else if ([call.method isEqualToString:@"map#getTileOverlayInfo"]) { + NSString* rawTileOverlayId = call.arguments[@"tileOverlayId"]; + result([_tileOverlaysController getTileOverlayInfo:rawTileOverlayId]); } else { result(FlutterMethodNotImplemented); } diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h index c7613fde5f93..eb1735d134b8 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h @@ -13,6 +13,7 @@ - (void)setStrokeColor:(UIColor*)color; - (void)setStrokeWidth:(CGFloat)width; - (void)setPoints:(NSArray*)points; +- (void)setHoles:(NSArray*>*)holes; - (void)setZIndex:(int)zIndex; @end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m index 678d40e3efec..1063a8cda990 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m @@ -45,6 +45,19 @@ - (void)setPoints:(NSArray*)points { } _polygon.path = path; } +- (void)setHoles:(NSArray*>*)rawHoles { + NSMutableArray* holes = [[NSMutableArray alloc] init]; + + for (NSArray* points in rawHoles) { + GMSMutablePath* path = [GMSMutablePath path]; + for (CLLocation* location in points) { + [path addCoordinate:location.coordinate]; + } + [holes addObject:path]; + } + + _polygon.holes = holes; +} - (void)setFillColor:(UIColor*)color { _polygon.fillColor = color; @@ -65,6 +78,10 @@ - (void)setStrokeWidth:(CGFloat)width { return [FLTGoogleMapJsonConversions toPoints:data]; } +static NSArray*>* ToHoles(NSArray* data) { + return [FLTGoogleMapJsonConversions toHoles:data]; +} + static UIColor* ToColor(NSNumber* data) { return [FLTGoogleMapJsonConversions toColor:data]; } static void InterpretPolygonOptions(NSDictionary* data, id sink, @@ -89,6 +106,11 @@ static void InterpretPolygonOptions(NSDictionary* data, id*)toPoints:(NSArray*)data; ++ (NSArray*>*)toHoles:(NSArray*)data; @end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.m index 6381beaee8d2..829f87791a07 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.m @@ -58,4 +58,14 @@ + (UIColor*)toColor:(NSNumber*)numberColor { return points; } ++ (NSArray*>*)toHoles:(NSArray*)data { + NSMutableArray*>* holes = [[[NSMutableArray alloc] init] init]; + for (unsigned i = 0; i < [data count]; i++) { + NSArray* points = [FLTGoogleMapJsonConversions toPoints:data[i]]; + [holes addObject:points]; + } + + return holes; +} + @end diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart index b879f3d302cf..703ba63e5ccc 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart @@ -13,7 +13,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; @@ -43,7 +42,11 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf PolygonId, Polyline, PolylineId, - ScreenCoordinate; + ScreenCoordinate, + Tile, + TileOverlayId, + TileOverlay, + TileProvider; part 'src/controller.dart'; part 'src/google_map.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index f47b8e57b049..9b61ec002ff9 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart @@ -4,9 +4,6 @@ part of google_maps_flutter; -final GoogleMapsFlutterPlatform _googleMapsFlutterPlatform = - GoogleMapsFlutterPlatform.instance; - /// Controller for a single GoogleMap instance running on the host platform. class GoogleMapController { /// The mapId for this controller @@ -15,8 +12,8 @@ class GoogleMapController { GoogleMapController._( CameraPosition initialCameraPosition, this._googleMapState, { - @required this.mapId, - }) : assert(_googleMapsFlutterPlatform != null) { + required this.mapId, + }) { _connectStreams(mapId); } @@ -30,7 +27,7 @@ class GoogleMapController { _GoogleMapState googleMapState, ) async { assert(id != null); - await _googleMapsFlutterPlatform.init(id); + await GoogleMapsFlutterPlatform.instance.init(id); return GoogleMapController._( initialCameraPosition, googleMapState, @@ -43,9 +40,10 @@ class GoogleMapController { /// Accessible only for testing. // TODO(dit) https://github.com/flutter/flutter/issues/55504 Remove this getter. @visibleForTesting - MethodChannel get channel { - if (_googleMapsFlutterPlatform is MethodChannelGoogleMapsFlutter) { - return (_googleMapsFlutterPlatform as MethodChannelGoogleMapsFlutter) + MethodChannel? get channel { + if (GoogleMapsFlutterPlatform.instance is MethodChannelGoogleMapsFlutter) { + return (GoogleMapsFlutterPlatform.instance + as MethodChannelGoogleMapsFlutter) .channel(mapId); } return null; @@ -55,40 +53,40 @@ class GoogleMapController { void _connectStreams(int mapId) { if (_googleMapState.widget.onCameraMoveStarted != null) { - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onCameraMoveStarted(mapId: mapId) - .listen((_) => _googleMapState.widget.onCameraMoveStarted()); + .listen((_) => _googleMapState.widget.onCameraMoveStarted!()); } if (_googleMapState.widget.onCameraMove != null) { - _googleMapsFlutterPlatform.onCameraMove(mapId: mapId).listen( - (CameraMoveEvent e) => _googleMapState.widget.onCameraMove(e.value)); + GoogleMapsFlutterPlatform.instance.onCameraMove(mapId: mapId).listen( + (CameraMoveEvent e) => _googleMapState.widget.onCameraMove!(e.value)); } if (_googleMapState.widget.onCameraIdle != null) { - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onCameraIdle(mapId: mapId) - .listen((_) => _googleMapState.widget.onCameraIdle()); + .listen((_) => _googleMapState.widget.onCameraIdle!()); } - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onMarkerTap(mapId: mapId) .listen((MarkerTapEvent e) => _googleMapState.onMarkerTap(e.value)); - _googleMapsFlutterPlatform.onMarkerDragEnd(mapId: mapId).listen( + GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen( (MarkerDragEndEvent e) => _googleMapState.onMarkerDragEnd(e.value, e.position)); - _googleMapsFlutterPlatform.onInfoWindowTap(mapId: mapId).listen( + GoogleMapsFlutterPlatform.instance.onInfoWindowTap(mapId: mapId).listen( (InfoWindowTapEvent e) => _googleMapState.onInfoWindowTap(e.value)); - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onPolylineTap(mapId: mapId) .listen((PolylineTapEvent e) => _googleMapState.onPolylineTap(e.value)); - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onPolygonTap(mapId: mapId) .listen((PolygonTapEvent e) => _googleMapState.onPolygonTap(e.value)); - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onCircleTap(mapId: mapId) .listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value)); - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onTap(mapId: mapId) .listen((MapTapEvent e) => _googleMapState.onTap(e.position)); - _googleMapsFlutterPlatform.onLongPress(mapId: mapId).listen( + GoogleMapsFlutterPlatform.instance.onLongPress(mapId: mapId).listen( (MapLongPressEvent e) => _googleMapState.onLongPress(e.position)); } @@ -100,8 +98,8 @@ class GoogleMapController { /// The returned [Future] completes after listeners have been notified. Future _updateMapOptions(Map optionsUpdate) { assert(optionsUpdate != null); - return _googleMapsFlutterPlatform.updateMapOptions(optionsUpdate, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .updateMapOptions(optionsUpdate, mapId: mapId); } /// Updates marker configuration. @@ -112,8 +110,8 @@ class GoogleMapController { /// The returned [Future] completes after listeners have been notified. Future _updateMarkers(MarkerUpdates markerUpdates) { assert(markerUpdates != null); - return _googleMapsFlutterPlatform.updateMarkers(markerUpdates, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .updateMarkers(markerUpdates, mapId: mapId); } /// Updates polygon configuration. @@ -124,8 +122,8 @@ class GoogleMapController { /// The returned [Future] completes after listeners have been notified. Future _updatePolygons(PolygonUpdates polygonUpdates) { assert(polygonUpdates != null); - return _googleMapsFlutterPlatform.updatePolygons(polygonUpdates, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .updatePolygons(polygonUpdates, mapId: mapId); } /// Updates polyline configuration. @@ -136,8 +134,8 @@ class GoogleMapController { /// The returned [Future] completes after listeners have been notified. Future _updatePolylines(PolylineUpdates polylineUpdates) { assert(polylineUpdates != null); - return _googleMapsFlutterPlatform.updatePolylines(polylineUpdates, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .updatePolylines(polylineUpdates, mapId: mapId); } /// Updates circle configuration. @@ -148,8 +146,32 @@ class GoogleMapController { /// The returned [Future] completes after listeners have been notified. Future _updateCircles(CircleUpdates circleUpdates) { assert(circleUpdates != null); - return _googleMapsFlutterPlatform.updateCircles(circleUpdates, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .updateCircles(circleUpdates, mapId: mapId); + } + + /// Updates tile overlays configuration. + /// + /// Change listeners are notified once the update has been made on the + /// platform side. + /// + /// The returned [Future] completes after listeners have been notified. + Future _updateTileOverlays(Set newTileOverlays) { + return GoogleMapsFlutterPlatform.instance + .updateTileOverlays(newTileOverlays: newTileOverlays, mapId: mapId); + } + + /// Clears the tile cache so that all tiles will be requested again from the + /// [TileProvider]. + /// + /// The current tiles from this tile overlay will also be + /// cleared from the map after calling this method. The API maintains a small + /// in-memory cache of tiles. If you want to cache tiles for longer, you + /// should implement an on-disk cache. + Future clearTileCache(TileOverlayId tileOverlayId) async { + assert(tileOverlayId != null); + return GoogleMapsFlutterPlatform.instance + .clearTileCache(tileOverlayId, mapId: mapId); } /// Starts an animated change of the map camera position. @@ -157,7 +179,8 @@ class GoogleMapController { /// The returned [Future] completes after the change has been started on the /// platform side. Future animateCamera(CameraUpdate cameraUpdate) { - return _googleMapsFlutterPlatform.animateCamera(cameraUpdate, mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .animateCamera(cameraUpdate, mapId: mapId); } /// Changes the map camera position. @@ -165,7 +188,8 @@ class GoogleMapController { /// The returned [Future] completes after the change has been made on the /// platform side. Future moveCamera(CameraUpdate cameraUpdate) { - return _googleMapsFlutterPlatform.moveCamera(cameraUpdate, mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .moveCamera(cameraUpdate, mapId: mapId); } /// Sets the styling of the base map. @@ -181,13 +205,14 @@ class GoogleMapController { /// Also, refer [iOS](https://developers.google.com/maps/documentation/ios-sdk/style-reference) /// and [Android](https://developers.google.com/maps/documentation/android-sdk/style-reference) /// style reference for more information regarding the supported styles. - Future setMapStyle(String mapStyle) { - return _googleMapsFlutterPlatform.setMapStyle(mapStyle, mapId: mapId); + Future setMapStyle(String? mapStyle) { + return GoogleMapsFlutterPlatform.instance + .setMapStyle(mapStyle, mapId: mapId); } /// Return [LatLngBounds] defining the region that is visible in a map. Future getVisibleRegion() { - return _googleMapsFlutterPlatform.getVisibleRegion(mapId: mapId); + return GoogleMapsFlutterPlatform.instance.getVisibleRegion(mapId: mapId); } /// Return [ScreenCoordinate] of the [LatLng] in the current map view. @@ -196,7 +221,8 @@ class GoogleMapController { /// Screen location is in screen pixels (not display pixels) with respect to the top left corner /// of the map, not necessarily of the whole screen. Future getScreenCoordinate(LatLng latLng) { - return _googleMapsFlutterPlatform.getScreenCoordinate(latLng, mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .getScreenCoordinate(latLng, mapId: mapId); } /// Returns [LatLng] corresponding to the [ScreenCoordinate] in the current map view. @@ -204,7 +230,8 @@ class GoogleMapController { /// Returned [LatLng] corresponds to a screen location. The screen location is specified in screen /// pixels (not display pixels) relative to the top left of the map, not top left of the whole screen. Future getLatLng(ScreenCoordinate screenCoordinate) { - return _googleMapsFlutterPlatform.getLatLng(screenCoordinate, mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .getLatLng(screenCoordinate, mapId: mapId); } /// Programmatically show the Info Window for a [Marker]. @@ -217,8 +244,8 @@ class GoogleMapController { /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. Future showMarkerInfoWindow(MarkerId markerId) { assert(markerId != null); - return _googleMapsFlutterPlatform.showMarkerInfoWindow(markerId, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .showMarkerInfoWindow(markerId, mapId: mapId); } /// Programmatically hide the Info Window for a [Marker]. @@ -231,8 +258,8 @@ class GoogleMapController { /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. Future hideMarkerInfoWindow(MarkerId markerId) { assert(markerId != null); - return _googleMapsFlutterPlatform.hideMarkerInfoWindow(markerId, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .hideMarkerInfoWindow(markerId, mapId: mapId); } /// Returns `true` when the [InfoWindow] is showing, `false` otherwise. @@ -245,22 +272,22 @@ class GoogleMapController { /// * [hideMarkerInfoWindow] to hide the Info Window. Future isMarkerInfoWindowShown(MarkerId markerId) { assert(markerId != null); - return _googleMapsFlutterPlatform.isMarkerInfoWindowShown(markerId, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .isMarkerInfoWindowShown(markerId, mapId: mapId); } /// Returns the current zoom level of the map Future getZoomLevel() { - return _googleMapsFlutterPlatform.getZoomLevel(mapId: mapId); + return GoogleMapsFlutterPlatform.instance.getZoomLevel(mapId: mapId); } /// Returns the image bytes of the map - Future takeSnapshot() { - return _googleMapsFlutterPlatform.takeSnapshot(mapId: mapId); + Future takeSnapshot() { + return GoogleMapsFlutterPlatform.instance.takeSnapshot(mapId: mapId); } /// Disposes of the platform resources void dispose() { - _googleMapsFlutterPlatform.dispose(mapId: mapId); + GoogleMapsFlutterPlatform.instance.dispose(mapId: mapId); } } diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index d7f0f1a4e280..deac00658d6b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart +++ b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart @@ -14,7 +14,29 @@ typedef void MapCreatedCallback(GoogleMapController controller); // to the buildView function, so the web implementation can use it as a // cache key. This needs to be provided from the outside, because web // views seem to re-render much more often that mobile platform views. -int _webOnlyMapId = 0; +int _nextMapCreationId = 0; + +/// Error thrown when an unknown map object ID is provided to a method. +class UnknownMapObjectIdError extends Error { + /// Creates an assertion error with the provided [message]. + UnknownMapObjectIdError(this.objectType, this.objectId, [this.context]); + + /// The name of the map object whose ID is unknown. + final String objectType; + + /// The unknown maps object ID. + final MapsObjectId objectId; + + /// The context where the error occurred. + final String? context; + + String toString() { + if (context != null) { + return 'Unknown $objectType ID "${objectId.value}" in $context'; + } + return 'Unknown $objectType ID "${objectId.value}"'; + } +} /// A widget which displays a map with data obtained from the Google Maps service. class GoogleMap extends StatefulWidget { @@ -22,10 +44,10 @@ class GoogleMap extends StatefulWidget { /// /// [AssertionError] will be thrown if [initialCameraPosition] is null; const GoogleMap({ - Key key, - @required this.initialCameraPosition, + Key? key, + required this.initialCameraPosition, this.onMapCreated, - this.gestureRecognizers, + this.gestureRecognizers = const >{}, this.compassEnabled = true, this.mapToolbarEnabled = true, this.cameraTargetBounds = CameraTargetBounds.unbounded, @@ -45,11 +67,12 @@ class GoogleMap extends StatefulWidget { this.indoorViewEnabled = false, this.trafficEnabled = false, this.buildingsEnabled = true, - this.markers, - this.polygons, - this.polylines, - this.circles, + this.markers = const {}, + this.polygons = const {}, + this.polylines = const {}, + this.circles = const {}, this.onCameraMoveStarted, + this.tileOverlays = const {}, this.onCameraMove, this.onCameraIdle, this.onTap, @@ -60,7 +83,7 @@ class GoogleMap extends StatefulWidget { /// Callback method for when the map is ready to be used. /// /// Used to receive a [GoogleMapController] for this [GoogleMap]. - final MapCreatedCallback onMapCreated; + final MapCreatedCallback? onMapCreated; /// The initial position of the map's camera. final CameraPosition initialCameraPosition; @@ -120,6 +143,9 @@ class GoogleMap extends StatefulWidget { /// Circles to be placed on the map. final Set circles; + /// Tile overlays to be placed on the map. + final Set tileOverlays; + /// Called when the camera starts moving. /// /// This can be initiated by the following: @@ -128,24 +154,24 @@ class GoogleMap extends StatefulWidget { /// 2. Programmatically initiated animation. /// 3. Camera motion initiated in response to user gestures on the map. /// For example: pan, tilt, pinch to zoom, or rotate. - final VoidCallback onCameraMoveStarted; + final VoidCallback? onCameraMoveStarted; /// Called repeatedly as the camera continues to move after an /// onCameraMoveStarted call. /// /// This may be called as often as once every frame and should /// not perform expensive operations. - final CameraPositionCallback onCameraMove; + final CameraPositionCallback? onCameraMove; /// Called when camera movement has ended, there are no pending /// animations and the user has stopped interacting with the map. - final VoidCallback onCameraIdle; + final VoidCallback? onCameraIdle; /// Called every time a [GoogleMap] is tapped. - final ArgumentCallback onTap; + final ArgumentCallback? onTap; /// Called every time a [GoogleMap] is long pressed. - final ArgumentCallback onLongPress; + final ArgumentCallback? onLongPress; /// True if a "My Location" layer should be shown on the map. /// @@ -201,7 +227,7 @@ class GoogleMap extends StatefulWidget { /// vertical drags. The map will claim gestures that are recognized by any of the /// recognizers on this list. /// - /// When this set is empty or null, the map will only handle pointer events for gestures that + /// When this set is empty, the map will only handle pointer events for gestures that /// were not claimed by any other gesture recognizer. final Set> gestureRecognizers; @@ -211,7 +237,7 @@ class GoogleMap extends StatefulWidget { } class _GoogleMapState extends State { - final _webOnlyMapCreationId = _webOnlyMapId++; + final _mapId = _nextMapCreationId++; final Completer _controller = Completer(); @@ -220,24 +246,20 @@ class _GoogleMapState extends State { Map _polygons = {}; Map _polylines = {}; Map _circles = {}; - _GoogleMapOptions _googleMapOptions; + late _GoogleMapOptions _googleMapOptions; @override Widget build(BuildContext context) { - final Map creationParams = { - 'initialCameraPosition': widget.initialCameraPosition?.toMap(), - 'options': _googleMapOptions.toMap(), - 'markersToAdd': serializeMarkerSet(widget.markers), - 'polygonsToAdd': serializePolygonSet(widget.polygons), - 'polylinesToAdd': serializePolylineSet(widget.polylines), - 'circlesToAdd': serializeCircleSet(widget.circles), - '_webOnlyMapCreationId': _webOnlyMapCreationId, - }; - - return _googleMapsFlutterPlatform.buildView( - creationParams, - widget.gestureRecognizers, + return GoogleMapsFlutterPlatform.instance.buildView( + _mapId, onPlatformViewCreated, + initialCameraPosition: widget.initialCameraPosition, + markers: widget.markers, + polygons: widget.polygons, + polylines: widget.polylines, + circles: widget.circles, + gestureRecognizers: widget.gestureRecognizers, + mapOptions: _googleMapOptions.toMap(), ); } @@ -266,6 +288,7 @@ class _GoogleMapState extends State { _updatePolygons(); _updatePolylines(); _updateCircles(); + _updateTileOverlays(); } void _updateOptions() async { @@ -313,6 +336,12 @@ class _GoogleMapState extends State { _circles = keyByCircleId(widget.circles); } + void _updateTileOverlays() async { + final GoogleMapController controller = await _controller.future; + // ignore: unawaited_futures + controller._updateTileOverlays(widget.tileOverlays); + } + Future onPlatformViewCreated(int id) async { final GoogleMapController controller = await GoogleMapController.init( id, @@ -320,116 +349,124 @@ class _GoogleMapState extends State { this, ); _controller.complete(controller); - if (widget.onMapCreated != null) { - widget.onMapCreated(controller); + _updateTileOverlays(); + final MapCreatedCallback? onMapCreated = widget.onMapCreated; + if (onMapCreated != null) { + onMapCreated(controller); } } void onMarkerTap(MarkerId markerId) { assert(markerId != null); - if (_markers[markerId]?.onTap != null) { - _markers[markerId].onTap(); + final Marker? marker = _markers[markerId]; + if (marker == null) { + throw UnknownMapObjectIdError('marker', markerId, 'onTap'); + } + final VoidCallback? onTap = marker.onTap; + if (onTap != null) { + onTap(); } } void onMarkerDragEnd(MarkerId markerId, LatLng position) { assert(markerId != null); - if (_markers[markerId]?.onDragEnd != null) { - _markers[markerId].onDragEnd(position); + final Marker? marker = _markers[markerId]; + if (marker == null) { + throw UnknownMapObjectIdError('marker', markerId, 'onDragEnd'); + } + final ValueChanged? onDragEnd = marker.onDragEnd; + if (onDragEnd != null) { + onDragEnd(position); } } void onPolygonTap(PolygonId polygonId) { assert(polygonId != null); - _polygons[polygonId].onTap(); + final Polygon? polygon = _polygons[polygonId]; + if (polygon == null) { + throw UnknownMapObjectIdError('polygon', polygonId, 'onTap'); + } + final VoidCallback? onTap = polygon.onTap; + if (onTap != null) { + onTap(); + } } void onPolylineTap(PolylineId polylineId) { assert(polylineId != null); - if (_polylines[polylineId]?.onTap != null) { - _polylines[polylineId].onTap(); + final Polyline? polyline = _polylines[polylineId]; + if (polyline == null) { + throw UnknownMapObjectIdError('polyline', polylineId, 'onTap'); + } + final VoidCallback? onTap = polyline.onTap; + if (onTap != null) { + onTap(); } } void onCircleTap(CircleId circleId) { assert(circleId != null); - _circles[circleId].onTap(); + final Circle? circle = _circles[circleId]; + if (circle == null) { + throw UnknownMapObjectIdError('marker', circleId, 'onTap'); + } + final VoidCallback? onTap = circle.onTap; + if (onTap != null) { + onTap(); + } } void onInfoWindowTap(MarkerId markerId) { assert(markerId != null); - if (_markers[markerId]?.infoWindow?.onTap != null) { - _markers[markerId].infoWindow.onTap(); + final Marker? marker = _markers[markerId]; + if (marker == null) { + throw UnknownMapObjectIdError('marker', markerId, 'InfoWindow onTap'); + } + final VoidCallback? onTap = marker.infoWindow.onTap; + if (onTap != null) { + onTap(); } } void onTap(LatLng position) { assert(position != null); - if (widget.onTap != null) { - widget.onTap(position); + final ArgumentCallback? onTap = widget.onTap; + if (onTap != null) { + onTap(position); } } void onLongPress(LatLng position) { assert(position != null); - if (widget.onLongPress != null) { - widget.onLongPress(position); + final ArgumentCallback? onLongPress = widget.onLongPress; + if (onLongPress != null) { + onLongPress(position); } } } /// Configuration options for the GoogleMaps user interface. -/// -/// When used to change configuration, null values will be interpreted as -/// "do not change this configuration option". class _GoogleMapOptions { - _GoogleMapOptions({ - this.compassEnabled, - this.mapToolbarEnabled, - this.cameraTargetBounds, - this.mapType, - this.minMaxZoomPreference, - this.rotateGesturesEnabled, - this.scrollGesturesEnabled, - this.tiltGesturesEnabled, - this.trackCameraPosition, - this.zoomControlsEnabled, - this.zoomGesturesEnabled, - this.liteModeEnabled, - this.myLocationEnabled, - this.myLocationButtonEnabled, - this.padding, - this.indoorViewEnabled, - this.trafficEnabled, - this.buildingsEnabled, - }) { - assert(liteModeEnabled == null || - !liteModeEnabled || - (liteModeEnabled && Platform.isAndroid)); - } - - static _GoogleMapOptions fromWidget(GoogleMap map) { - return _GoogleMapOptions( - compassEnabled: map.compassEnabled, - mapToolbarEnabled: map.mapToolbarEnabled, - cameraTargetBounds: map.cameraTargetBounds, - mapType: map.mapType, - minMaxZoomPreference: map.minMaxZoomPreference, - rotateGesturesEnabled: map.rotateGesturesEnabled, - scrollGesturesEnabled: map.scrollGesturesEnabled, - tiltGesturesEnabled: map.tiltGesturesEnabled, - trackCameraPosition: map.onCameraMove != null, - zoomControlsEnabled: map.zoomControlsEnabled, - zoomGesturesEnabled: map.zoomGesturesEnabled, - liteModeEnabled: map.liteModeEnabled, - myLocationEnabled: map.myLocationEnabled, - myLocationButtonEnabled: map.myLocationButtonEnabled, - padding: map.padding, - indoorViewEnabled: map.indoorViewEnabled, - trafficEnabled: map.trafficEnabled, - buildingsEnabled: map.buildingsEnabled, - ); - } + _GoogleMapOptions.fromWidget(GoogleMap map) + : compassEnabled = map.compassEnabled, + mapToolbarEnabled = map.mapToolbarEnabled, + cameraTargetBounds = map.cameraTargetBounds, + mapType = map.mapType, + minMaxZoomPreference = map.minMaxZoomPreference, + rotateGesturesEnabled = map.rotateGesturesEnabled, + scrollGesturesEnabled = map.scrollGesturesEnabled, + tiltGesturesEnabled = map.tiltGesturesEnabled, + trackCameraPosition = map.onCameraMove != null, + zoomControlsEnabled = map.zoomControlsEnabled, + zoomGesturesEnabled = map.zoomGesturesEnabled, + liteModeEnabled = map.liteModeEnabled, + myLocationEnabled = map.myLocationEnabled, + myLocationButtonEnabled = map.myLocationButtonEnabled, + padding = map.padding, + indoorViewEnabled = map.indoorViewEnabled, + trafficEnabled = map.trafficEnabled, + buildingsEnabled = map.buildingsEnabled, + assert(!map.liteModeEnabled || Platform.isAndroid); final bool compassEnabled; @@ -468,38 +505,31 @@ class _GoogleMapOptions { final bool buildingsEnabled; Map toMap() { - final Map optionsMap = {}; - - void addIfNonNull(String fieldName, dynamic value) { - if (value != null) { - optionsMap[fieldName] = value; - } - } - - addIfNonNull('compassEnabled', compassEnabled); - addIfNonNull('mapToolbarEnabled', mapToolbarEnabled); - addIfNonNull('cameraTargetBounds', cameraTargetBounds?.toJson()); - addIfNonNull('mapType', mapType?.index); - addIfNonNull('minMaxZoomPreference', minMaxZoomPreference?.toJson()); - addIfNonNull('rotateGesturesEnabled', rotateGesturesEnabled); - addIfNonNull('scrollGesturesEnabled', scrollGesturesEnabled); - addIfNonNull('tiltGesturesEnabled', tiltGesturesEnabled); - addIfNonNull('zoomControlsEnabled', zoomControlsEnabled); - addIfNonNull('zoomGesturesEnabled', zoomGesturesEnabled); - addIfNonNull('liteModeEnabled', liteModeEnabled); - addIfNonNull('trackCameraPosition', trackCameraPosition); - addIfNonNull('myLocationEnabled', myLocationEnabled); - addIfNonNull('myLocationButtonEnabled', myLocationButtonEnabled); - addIfNonNull('padding', [ - padding?.top, - padding?.left, - padding?.bottom, - padding?.right, - ]); - addIfNonNull('indoorEnabled', indoorViewEnabled); - addIfNonNull('trafficEnabled', trafficEnabled); - addIfNonNull('buildingsEnabled', buildingsEnabled); - return optionsMap; + return { + 'compassEnabled': compassEnabled, + 'mapToolbarEnabled': mapToolbarEnabled, + 'cameraTargetBounds': cameraTargetBounds.toJson(), + 'mapType': mapType.index, + 'minMaxZoomPreference': minMaxZoomPreference.toJson(), + 'rotateGesturesEnabled': rotateGesturesEnabled, + 'scrollGesturesEnabled': scrollGesturesEnabled, + 'tiltGesturesEnabled': tiltGesturesEnabled, + 'zoomControlsEnabled': zoomControlsEnabled, + 'zoomGesturesEnabled': zoomGesturesEnabled, + 'liteModeEnabled': liteModeEnabled, + 'trackCameraPosition': trackCameraPosition, + 'myLocationEnabled': myLocationEnabled, + 'myLocationButtonEnabled': myLocationButtonEnabled, + 'padding': [ + padding.top, + padding.left, + padding.bottom, + padding.right, + ], + 'indoorEnabled': indoorViewEnabled, + 'trafficEnabled': trafficEnabled, + 'buildingsEnabled': buildingsEnabled, + }; } Map updatesMap(_GoogleMapOptions newOptions) { diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 2e4ae9dca5d1..3d0e79473a33 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -1,13 +1,13 @@ 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.6 +version: 2.0.0 dependencies: flutter: sdk: flutter - flutter_plugin_android_lifecycle: ^1.0.0 - google_maps_flutter_platform_interface: ^1.0.4 + flutter_plugin_android_lifecycle: ^2.0.0 + google_maps_flutter_platform_interface: ^2.0.0 dev_dependencies: flutter_test: @@ -17,10 +17,10 @@ dev_dependencies: # https://github.com/dart-lang/pub/issues/2101 is resolved. flutter_driver: sdk: flutter - test: ^1.6.0 - pedantic: ^1.8.0 - plugin_platform_interface: ^1.0.2 - mockito: ^4.1.1 + test: ^1.16.0 + pedantic: ^1.10.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" + stream_transform: ^2.0.0 flutter: plugin: @@ -32,5 +32,5 @@ flutter: pluginClass: FLTGoogleMapsPlugin environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.22.0 <2.0.0" + sdk: '>=2.12.0-259.9.beta <3.0.0' + flutter: ">=1.22.0" diff --git a/packages/google_maps_flutter/google_maps_flutter/test/android_google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/android_google_map_test.dart index 194efe9a66f0..2399b7b15eff 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/android_google_map_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/android_google_map_test.dart @@ -37,7 +37,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.liteModeEnabled, false); diff --git a/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart index 3533ceb229e3..d61526b2bbeb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart @@ -9,20 +9,6 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'fake_maps_controllers.dart'; -Set _toSet({Circle c1, Circle c2, Circle c3}) { - final Set res = Set.identity(); - if (c1 != null) { - res.add(c1); - } - if (c2 != null) { - res.add(c2); - } - if (c3 != null) { - res.add(c3); - } - return res; -} - Widget _mapWithCircles(Set circles) { return Directionality( textDirection: TextDirection.ltr, @@ -50,10 +36,10 @@ void main() { testWidgets('Initializing a circle', (WidgetTester tester) async { final Circle c1 = Circle(circleId: CircleId("circle_1")); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c1))); + await tester.pumpWidget(_mapWithCircles({c1})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToAdd.length, 1); final Circle initializedCircle = platformGoogleMap.circlesToAdd.first; @@ -66,11 +52,11 @@ void main() { final Circle c1 = Circle(circleId: CircleId("circle_1")); final Circle c2 = Circle(circleId: CircleId("circle_2")); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c1))); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c1, c2: c2))); + await tester.pumpWidget(_mapWithCircles({c1})); + await tester.pumpWidget(_mapWithCircles({c1, c2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToAdd.length, 1); final Circle addedCircle = platformGoogleMap.circlesToAdd.first; @@ -84,11 +70,11 @@ void main() { testWidgets("Removing a circle", (WidgetTester tester) async { final Circle c1 = Circle(circleId: CircleId("circle_1")); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c1))); - await tester.pumpWidget(_mapWithCircles(null)); + await tester.pumpWidget(_mapWithCircles({c1})); + await tester.pumpWidget(_mapWithCircles({})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circleIdsToRemove.length, 1); expect(platformGoogleMap.circleIdsToRemove.first, equals(c1.circleId)); @@ -100,11 +86,11 @@ void main() { final Circle c1 = Circle(circleId: CircleId("circle_1")); final Circle c2 = Circle(circleId: CircleId("circle_1"), radius: 10); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c1))); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c2))); + await tester.pumpWidget(_mapWithCircles({c1})); + await tester.pumpWidget(_mapWithCircles({c2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToChange.length, 1); expect(platformGoogleMap.circlesToChange.first, equals(c2)); @@ -116,11 +102,11 @@ void main() { final Circle c1 = Circle(circleId: CircleId("circle_1")); final Circle c2 = Circle(circleId: CircleId("circle_1"), radius: 10); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c1))); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c2))); + await tester.pumpWidget(_mapWithCircles({c1})); + await tester.pumpWidget(_mapWithCircles({c2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToChange.length, 1); final Circle update = platformGoogleMap.circlesToChange.first; @@ -131,16 +117,16 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Circle c1 = Circle(circleId: CircleId("circle_1")); Circle c2 = Circle(circleId: CircleId("circle_2")); - final Set prev = _toSet(c1: c1, c2: c2); + final Set prev = {c1, c2}; c1 = Circle(circleId: CircleId("circle_1"), visible: false); c2 = Circle(circleId: CircleId("circle_2"), radius: 10); - final Set cur = _toSet(c1: c1, c2: c2); + final Set cur = {c1, c2}; await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToChange, cur); expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); @@ -150,18 +136,18 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Circle c2 = Circle(circleId: CircleId("circle_2")); final Circle c3 = Circle(circleId: CircleId("circle_3")); - final Set prev = _toSet(c2: c2, c3: c3); + final Set prev = {c2, c3}; // c1 is added, c2 is updated, c3 is removed. final Circle c1 = Circle(circleId: CircleId("circle_1")); c2 = Circle(circleId: CircleId("circle_2"), radius: 10); - final Set cur = _toSet(c1: c1, c2: c2); + final Set cur = {c1, c2}; await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToChange.length, 1); expect(platformGoogleMap.circlesToAdd.length, 1); @@ -176,32 +162,32 @@ void main() { final Circle c1 = Circle(circleId: CircleId("circle_1")); final Circle c2 = Circle(circleId: CircleId("circle_2")); Circle c3 = Circle(circleId: CircleId("circle_3")); - final Set prev = _toSet(c1: c1, c2: c2, c3: c3); + final Set prev = {c1, c2, c3}; c3 = Circle(circleId: CircleId("circle_3"), radius: 10); - final Set cur = _toSet(c1: c1, c2: c2, c3: c3); + final Set cur = {c1, c2, c3}; await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.circlesToChange, _toSet(c3: c3)); + expect(platformGoogleMap.circlesToChange, {c3}); expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); expect(platformGoogleMap.circlesToAdd.isEmpty, true); }); testWidgets("Update non platform related attr", (WidgetTester tester) async { Circle c1 = Circle(circleId: CircleId("circle_1")); - final Set prev = _toSet(c1: c1); + final Set prev = {c1}; c1 = Circle(circleId: CircleId("circle_1"), onTap: () => print("hello")); - final Set cur = _toSet(c1: c1); + final Set cur = {c1}; await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToChange.isEmpty, true); expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart index adca8b4c2a0e..8bc0fbb1d86e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart @@ -9,79 +9,87 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class FakePlatformGoogleMap { - FakePlatformGoogleMap(int id, Map params) { - cameraPosition = CameraPosition.fromMap(params['initialCameraPosition']); - channel = MethodChannel( - 'plugins.flutter.io/google_maps_$id', const StandardMethodCodec()); + FakePlatformGoogleMap(int id, Map params) + : cameraPosition = + CameraPosition.fromMap(params['initialCameraPosition']), + channel = MethodChannel( + 'plugins.flutter.io/google_maps_$id', const StandardMethodCodec()) { channel.setMockMethodCallHandler(onMethodCall); updateOptions(params['options']); updateMarkers(params); updatePolygons(params); updatePolylines(params); updateCircles(params); + updateTileOverlays(Map.castFrom(params)); } MethodChannel channel; - CameraPosition cameraPosition; + CameraPosition? cameraPosition; - bool compassEnabled; + bool? compassEnabled; - bool mapToolbarEnabled; + bool? mapToolbarEnabled; - CameraTargetBounds cameraTargetBounds; + CameraTargetBounds? cameraTargetBounds; - MapType mapType; + MapType? mapType; - MinMaxZoomPreference minMaxZoomPreference; + MinMaxZoomPreference? minMaxZoomPreference; - bool rotateGesturesEnabled; + bool? rotateGesturesEnabled; - bool scrollGesturesEnabled; + bool? scrollGesturesEnabled; - bool tiltGesturesEnabled; + bool? tiltGesturesEnabled; - bool zoomGesturesEnabled; + bool? zoomGesturesEnabled; - bool zoomControlsEnabled; + bool? zoomControlsEnabled; - bool liteModeEnabled; + bool? liteModeEnabled; - bool trackCameraPosition; + bool? trackCameraPosition; - bool myLocationEnabled; + bool? myLocationEnabled; - bool trafficEnabled; + bool? trafficEnabled; - bool buildingsEnabled; + bool? buildingsEnabled; - bool myLocationButtonEnabled; + bool? myLocationButtonEnabled; - List padding; + List? padding; - Set markerIdsToRemove; + Set markerIdsToRemove = {}; - Set markersToAdd; + Set markersToAdd = {}; - Set markersToChange; + Set markersToChange = {}; - Set polygonIdsToRemove; + Set polygonIdsToRemove = {}; - Set polygonsToAdd; + Set polygonsToAdd = {}; - Set polygonsToChange; + Set polygonsToChange = {}; - Set polylineIdsToRemove; + Set polylineIdsToRemove = {}; - Set polylinesToAdd; + Set polylinesToAdd = {}; - Set polylinesToChange; + Set polylinesToChange = {}; - Set circleIdsToRemove; + Set circleIdsToRemove = {}; - Set circlesToAdd; + Set circlesToAdd = {}; - Set circlesToChange; + Set circlesToChange = {}; + + Set tileOverlayIdsToRemove = {}; + + Set tileOverlaysToAdd = {}; + + Set tileOverlaysToChange = {}; Future onMethodCall(MethodCall call) { switch (call.method) { @@ -97,6 +105,10 @@ class FakePlatformGoogleMap { case 'polylines#update': updatePolylines(call.arguments); return Future.sync(() {}); + case 'tileOverlays#update': + updateTileOverlays( + Map.castFrom(call.arguments)); + return Future.sync(() {}); case 'circles#update': updateCircles(call.arguments); return Future.sync(() {}); @@ -105,7 +117,7 @@ class FakePlatformGoogleMap { } } - void updateMarkers(Map markerUpdates) { + void updateMarkers(Map? markerUpdates) { if (markerUpdates == null) { return; } @@ -115,29 +127,21 @@ class FakePlatformGoogleMap { markersToChange = _deserializeMarkers(markerUpdates['markersToChange']); } - Set _deserializeMarkerIds(List markerIds) { + Set _deserializeMarkerIds(List? markerIds) { if (markerIds == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } return markerIds.map((dynamic markerId) => MarkerId(markerId)).toSet(); } Set _deserializeMarkers(dynamic markers) { if (markers == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } final List markersData = markers; - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - final Set result = Set(); - for (Map markerData in markersData) { + final Set result = {}; + for (Map markerData + in markersData.cast>()) { final String markerId = markerData['markerId']; final double alpha = markerData['alpha']; final bool draggable = markerData['draggable']; @@ -165,7 +169,7 @@ class FakePlatformGoogleMap { return result; } - void updatePolygons(Map polygonUpdates) { + void updatePolygons(Map? polygonUpdates) { if (polygonUpdates == null) { return; } @@ -175,39 +179,33 @@ class FakePlatformGoogleMap { polygonsToChange = _deserializePolygons(polygonUpdates['polygonsToChange']); } - Set _deserializePolygonIds(List polygonIds) { + Set _deserializePolygonIds(List? polygonIds) { if (polygonIds == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } return polygonIds.map((dynamic polygonId) => PolygonId(polygonId)).toSet(); } Set _deserializePolygons(dynamic polygons) { if (polygons == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } final List polygonsData = polygons; - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - final Set result = Set(); - for (Map polygonData in polygonsData) { + final Set result = {}; + for (Map polygonData + in polygonsData.cast>()) { final String polygonId = polygonData['polygonId']; final bool visible = polygonData['visible']; final bool geodesic = polygonData['geodesic']; final List points = _deserializePoints(polygonData['points']); + final List> holes = _deserializeHoles(polygonData['holes']); result.add(Polygon( polygonId: PolygonId(polygonId), visible: visible, geodesic: geodesic, points: points, + holes: holes, )); } @@ -220,7 +218,15 @@ class FakePlatformGoogleMap { }).toList(); } - void updatePolylines(Map polylineUpdates) { + List> _deserializeHoles(List holes) { + return holes.map>((dynamic hole) { + return hole.map((dynamic list) { + return LatLng(list[0], list[1]); + }).toList(); + }).toList(); + } + + void updatePolylines(Map? polylineUpdates) { if (polylineUpdates == null) { return; } @@ -231,12 +237,9 @@ class FakePlatformGoogleMap { _deserializePolylines(polylineUpdates['polylinesToChange']); } - Set _deserializePolylineIds(List polylineIds) { + Set _deserializePolylineIds(List? polylineIds) { if (polylineIds == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } return polylineIds .map((dynamic polylineId) => PolylineId(polylineId)) @@ -245,17 +248,12 @@ class FakePlatformGoogleMap { Set _deserializePolylines(dynamic polylines) { if (polylines == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } final List polylinesData = polylines; - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - final Set result = Set(); - for (Map polylineData in polylinesData) { + final Set result = {}; + for (Map polylineData + in polylinesData.cast>()) { final String polylineId = polylineData['polylineId']; final bool visible = polylineData['visible']; final bool geodesic = polylineData['geodesic']; @@ -272,7 +270,7 @@ class FakePlatformGoogleMap { return result; } - void updateCircles(Map circleUpdates) { + void updateCircles(Map? circleUpdates) { if (circleUpdates == null) { return; } @@ -282,29 +280,46 @@ class FakePlatformGoogleMap { circlesToChange = _deserializeCircles(circleUpdates['circlesToChange']); } - Set _deserializeCircleIds(List circleIds) { + void updateTileOverlays(Map updateTileOverlayUpdates) { + if (updateTileOverlayUpdates == null) { + return; + } + final List>? tileOverlaysToAddList = + updateTileOverlayUpdates['tileOverlaysToAdd'] != null + ? List.castFrom>( + updateTileOverlayUpdates['tileOverlaysToAdd']) + : null; + final List? tileOverlayIdsToRemoveList = + updateTileOverlayUpdates['tileOverlayIdsToRemove'] != null + ? List.castFrom( + updateTileOverlayUpdates['tileOverlayIdsToRemove']) + : null; + final List>? tileOverlaysToChangeList = + updateTileOverlayUpdates['tileOverlaysToChange'] != null + ? List.castFrom>( + updateTileOverlayUpdates['tileOverlaysToChange']) + : null; + tileOverlaysToAdd = _deserializeTileOverlays(tileOverlaysToAddList); + tileOverlayIdsToRemove = + _deserializeTileOverlayIds(tileOverlayIdsToRemoveList); + tileOverlaysToChange = _deserializeTileOverlays(tileOverlaysToChangeList); + } + + Set _deserializeCircleIds(List? circleIds) { if (circleIds == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } return circleIds.map((dynamic circleId) => CircleId(circleId)).toSet(); } Set _deserializeCircles(dynamic circles) { if (circles == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } final List circlesData = circles; - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - final Set result = Set(); - for (Map circleData in circlesData) { + final Set result = {}; + for (Map circleData + in circlesData.cast>()) { final String circleId = circleData['circleId']; final bool visible = circleData['visible']; final double radius = circleData['radius']; @@ -319,6 +334,40 @@ class FakePlatformGoogleMap { return result; } + Set _deserializeTileOverlayIds(List? tileOverlayIds) { + if (tileOverlayIds == null || tileOverlayIds.isEmpty) { + return {}; + } + return tileOverlayIds + .map((String tileOverlayId) => TileOverlayId(tileOverlayId)) + .toSet(); + } + + Set _deserializeTileOverlays( + List>? tileOverlays) { + if (tileOverlays == null || tileOverlays.isEmpty) { + return {}; + } + final Set result = {}; + for (Map tileOverlayData in tileOverlays) { + final String tileOverlayId = tileOverlayData['tileOverlayId']; + final bool fadeIn = tileOverlayData['fadeIn']; + final double transparency = tileOverlayData['transparency']; + final int zIndex = tileOverlayData['zIndex']; + final bool visible = tileOverlayData['visible']; + + result.add(TileOverlay( + tileOverlayId: TileOverlayId(tileOverlayId), + fadeIn: fadeIn, + transparency: transparency, + zIndex: zIndex, + visible: visible, + )); + } + + return result; + } + void updateOptions(Map options) { if (options.containsKey('compassEnabled')) { compassEnabled = options['compassEnabled']; @@ -380,13 +429,13 @@ class FakePlatformGoogleMap { } class FakePlatformViewsController { - FakePlatformGoogleMap lastCreatedView; + FakePlatformGoogleMap? 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 = FakePlatformGoogleMap( args['id'], params, @@ -402,7 +451,7 @@ class FakePlatformViewsController { } } -Map _decodeParams(Uint8List paramsMessage) { +Map? _decodeParams(Uint8List paramsMessage) { final ByteBuffer buffer = paramsMessage.buffer; final ByteData messageBytes = buffer.asByteData( paramsMessage.offsetInBytes, diff --git a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart index 3c1eadb8d2a4..857344f5ac5e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart @@ -35,7 +35,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.cameraPosition, const CameraPosition(target: LatLng(10.0, 15.0))); @@ -62,7 +62,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.cameraPosition, const CameraPosition(target: LatLng(10.0, 15.0))); @@ -80,7 +80,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.compassEnabled, false); @@ -109,7 +109,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.mapToolbarEnabled, false); @@ -144,7 +144,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect( platformGoogleMap.cameraTargetBounds, @@ -193,7 +193,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.mapType, MapType.hybrid); @@ -222,7 +222,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.minMaxZoomPreference, const MinMaxZoomPreference(1.0, 3.0)); @@ -253,7 +253,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.rotateGesturesEnabled, false); @@ -282,7 +282,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.scrollGesturesEnabled, false); @@ -311,7 +311,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.tiltGesturesEnabled, false); @@ -339,7 +339,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.trackCameraPosition, false); @@ -369,7 +369,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.zoomGesturesEnabled, false); @@ -398,7 +398,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.zoomControlsEnabled, false); @@ -427,7 +427,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.myLocationEnabled, false); @@ -457,7 +457,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.myLocationButtonEnabled, true); @@ -485,7 +485,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.padding, [0, 0, 0, 0]); }); @@ -501,7 +501,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.padding, [0, 0, 0, 0]); @@ -542,7 +542,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.trafficEnabled, false); @@ -571,7 +571,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.buildingsEnabled, false); diff --git a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart index 5ea9a679a1be..684e8659c4b5 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart @@ -2,26 +2,26 @@ // 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/gestures.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:mockito/mockito.dart'; - -class MockGoogleMapsFlutterPlatform extends Mock - with MockPlatformInterfaceMixin - implements GoogleMapsFlutterPlatform {} +import 'package:stream_transform/stream_transform.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final platform = MockGoogleMapsFlutterPlatform(); + late TestGoogleMapsFlutterPlatform platform; setUp(() { // Use a mock platform so we never need to hit the MethodChannel code. + platform = TestGoogleMapsFlutterPlatform(); GoogleMapsFlutterPlatform.instance = platform; - resetMockitoState(); - _setupMock(platform); }); testWidgets('_webOnlyMapCreationId increments with each GoogleMap widget', ( @@ -49,18 +49,9 @@ void main() { ); // Verify that each one was created with a different _webOnlyMapCreationId. - verifyInOrder([ - platform.buildView( - argThat(containsPair('_webOnlyMapCreationId', 0)), - any, - any, - ), - platform.buildView( - argThat(containsPair('_webOnlyMapCreationId', 1)), - any, - any, - ), - ]); + expect(platform.createdIds.length, 2); + expect(platform.createdIds[0], 0); + expect(platform.createdIds[1], 1); }); testWidgets('Calls platform.dispose when GoogleMap is disposed of', ( @@ -75,47 +66,220 @@ void main() { // Now dispose of the map... await tester.pumpWidget(Container()); - verify(platform.dispose(mapId: anyNamed('mapId'))); + expect(platform.disposed, true); }); } -// Some test setup classes below... +// A dummy implementation of the platform interface for tests. +class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { + TestGoogleMapsFlutterPlatform(); + + // The IDs passed to each call to buildView, in call order. + List createdIds = []; + + // Whether `dispose` has been called. + bool disposed = false; + + // Stream controller to inject events for testing. + final StreamController mapEventStreamController = + StreamController.broadcast(); + + @override + Future init(int mapId) async {} + + @override + Future updateMapOptions( + Map optionsUpdate, { + required int mapId, + }) async {} + + @override + Future updateMarkers( + MarkerUpdates markerUpdates, { + required int mapId, + }) async {} + + @override + Future updatePolygons( + PolygonUpdates polygonUpdates, { + required int mapId, + }) async {} + + @override + Future updatePolylines( + PolylineUpdates polylineUpdates, { + required int mapId, + }) async {} + + @override + Future updateCircles( + CircleUpdates circleUpdates, { + required int mapId, + }) async {} + + @override + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) async {} + + @override + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) async {} + + @override + Future animateCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async {} + + @override + Future moveCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async {} + + @override + Future setMapStyle( + String? mapStyle, { + required int mapId, + }) async {} + + @override + Future getVisibleRegion({ + required int mapId, + }) async { + return LatLngBounds(southwest: LatLng(0, 0), northeast: LatLng(0, 0)); + } -class _MockStream extends Mock implements Stream {} + @override + Future getScreenCoordinate( + LatLng latLng, { + required int mapId, + }) async { + return ScreenCoordinate(x: 0, y: 0); + } -typedef _CreationCallback = void Function(int); + @override + Future getLatLng( + ScreenCoordinate screenCoordinate, { + required int mapId, + }) async { + return LatLng(0, 0); + } -// Installs test mocks on the platform -void _setupMock(MockGoogleMapsFlutterPlatform platform) { - // Used to create the view of the map... - when(platform.buildView(any, any, any)).thenAnswer((realInvocation) { - // Call the onPlatformViewCreated callback so the controller gets created. - _CreationCallback onPlatformViewCreatedCb = - realInvocation.positionalArguments[2]; - onPlatformViewCreatedCb.call(0); + @override + Future showMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async {} + + @override + Future hideMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async {} + + @override + Future isMarkerInfoWindowShown( + MarkerId markerId, { + required int mapId, + }) async { + return false; + } + + @override + Future getZoomLevel({ + required int mapId, + }) async { + return 0.0; + } + + @override + Future takeSnapshot({ + required int mapId, + }) async { + return null; + } + + @override + Stream onCameraMoveStarted({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCameraMove({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCameraIdle({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onInfoWindowTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDragEnd({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onPolylineTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onPolygonTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCircleTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onLongPress({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + void dispose({required int mapId}) { + disposed = true; + } + + @override + Widget buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers = + const >{}, + Map mapOptions = const {}, + }) { + onPlatformViewCreated(0); + createdIds.add(creationId); return Container(); - }); - // Used to create the Controller - when(platform.onCameraIdle(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onCameraMove(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onCameraMoveStarted(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onCircleTap(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onInfoWindowTap(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onLongPress(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onMarkerDragEnd(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onMarkerTap(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onPolygonTap(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onPolylineTap(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onTap(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart index 620e1ef4bfea..ce0da4235c9c 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart @@ -9,20 +9,6 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'fake_maps_controllers.dart'; -Set _toSet({Marker m1, Marker m2, Marker m3}) { - final Set res = Set.identity(); - if (m1 != null) { - res.add(m1); - } - if (m2 != null) { - res.add(m2); - } - if (m3 != null) { - res.add(m3); - } - return res; -} - Widget _mapWithMarkers(Set markers) { return Directionality( textDirection: TextDirection.ltr, @@ -50,10 +36,10 @@ void main() { testWidgets('Initializing a marker', (WidgetTester tester) async { final Marker m1 = Marker(markerId: MarkerId("marker_1")); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1))); + await tester.pumpWidget(_mapWithMarkers({m1})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToAdd.length, 1); final Marker initializedMarker = platformGoogleMap.markersToAdd.first; @@ -66,11 +52,11 @@ void main() { final Marker m1 = Marker(markerId: MarkerId("marker_1")); final Marker m2 = Marker(markerId: MarkerId("marker_2")); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1))); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1, m2: m2))); + await tester.pumpWidget(_mapWithMarkers({m1})); + await tester.pumpWidget(_mapWithMarkers({m1, m2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToAdd.length, 1); final Marker addedMarker = platformGoogleMap.markersToAdd.first; @@ -84,11 +70,11 @@ void main() { testWidgets("Removing a marker", (WidgetTester tester) async { final Marker m1 = Marker(markerId: MarkerId("marker_1")); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1))); - await tester.pumpWidget(_mapWithMarkers(null)); + await tester.pumpWidget(_mapWithMarkers({m1})); + await tester.pumpWidget(_mapWithMarkers({})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markerIdsToRemove.length, 1); expect(platformGoogleMap.markerIdsToRemove.first, equals(m1.markerId)); @@ -100,11 +86,11 @@ void main() { final Marker m1 = Marker(markerId: MarkerId("marker_1")); final Marker m2 = Marker(markerId: MarkerId("marker_1"), alpha: 0.5); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1))); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m2))); + await tester.pumpWidget(_mapWithMarkers({m1})); + await tester.pumpWidget(_mapWithMarkers({m2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToChange.length, 1); expect(platformGoogleMap.markersToChange.first, equals(m2)); @@ -119,11 +105,11 @@ void main() { infoWindow: const InfoWindow(snippet: 'changed'), ); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1))); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m2))); + await tester.pumpWidget(_mapWithMarkers({m1})); + await tester.pumpWidget(_mapWithMarkers({m2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToChange.length, 1); final Marker update = platformGoogleMap.markersToChange.first; @@ -134,16 +120,16 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Marker m1 = Marker(markerId: MarkerId("marker_1")); Marker m2 = Marker(markerId: MarkerId("marker_2")); - final Set prev = _toSet(m1: m1, m2: m2); + final Set prev = {m1, m2}; m1 = Marker(markerId: MarkerId("marker_1"), visible: false); m2 = Marker(markerId: MarkerId("marker_2"), draggable: true); - final Set cur = _toSet(m1: m1, m2: m2); + final Set cur = {m1, m2}; await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToChange, cur); expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); @@ -153,18 +139,18 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Marker m2 = Marker(markerId: MarkerId("marker_2")); final Marker m3 = Marker(markerId: MarkerId("marker_3")); - final Set prev = _toSet(m2: m2, m3: m3); + final Set prev = {m2, m3}; // m1 is added, m2 is updated, m3 is removed. final Marker m1 = Marker(markerId: MarkerId("marker_1")); m2 = Marker(markerId: MarkerId("marker_2"), draggable: true); - final Set cur = _toSet(m1: m1, m2: m2); + final Set cur = {m1, m2}; await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToChange.length, 1); expect(platformGoogleMap.markersToAdd.length, 1); @@ -179,35 +165,35 @@ void main() { final Marker m1 = Marker(markerId: MarkerId("marker_1")); final Marker m2 = Marker(markerId: MarkerId("marker_2")); Marker m3 = Marker(markerId: MarkerId("marker_3")); - final Set prev = _toSet(m1: m1, m2: m2, m3: m3); + final Set prev = {m1, m2, m3}; m3 = Marker(markerId: MarkerId("marker_3"), draggable: true); - final Set cur = _toSet(m1: m1, m2: m2, m3: m3); + final Set cur = {m1, m2, m3}; await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.markersToChange, _toSet(m3: m3)); + expect(platformGoogleMap.markersToChange, {m3}); expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); expect(platformGoogleMap.markersToAdd.isEmpty, true); }); testWidgets("Update non platform related attr", (WidgetTester tester) async { Marker m1 = Marker(markerId: MarkerId("marker_1")); - final Set prev = _toSet(m1: m1); + final Set prev = {m1}; m1 = Marker( markerId: MarkerId("marker_1"), onTap: () => print("hello"), onDragEnd: (LatLng latLng) => print(latLng)); - final Set cur = _toSet(m1: m1); + final Set cur = {m1}; await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToChange.isEmpty, true); expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); diff --git a/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart index 185c996113af..e187426bf7f6 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart @@ -9,20 +9,6 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'fake_maps_controllers.dart'; -Set _toSet({Polygon p1, Polygon p2, Polygon p3}) { - final Set res = Set.identity(); - if (p1 != null) { - res.add(p1); - } - if (p2 != null) { - res.add(p2); - } - if (p3 != null) { - res.add(p3); - } - return res; -} - Widget _mapWithPolygons(Set polygons) { return Directionality( textDirection: TextDirection.ltr, @@ -33,6 +19,29 @@ Widget _mapWithPolygons(Set polygons) { ); } +List _rectPoints({ + required double size, + LatLng center = const LatLng(0, 0), +}) { + final halfSize = size / 2; + + return [ + LatLng(center.latitude + halfSize, center.longitude + halfSize), + LatLng(center.latitude - halfSize, center.longitude + halfSize), + LatLng(center.latitude - halfSize, center.longitude - halfSize), + LatLng(center.latitude + halfSize, center.longitude - halfSize), + ]; +} + +Polygon _polygonWithPointsAndHole(PolygonId polygonId) { + _rectPoints(size: 1); + return Polygon( + polygonId: polygonId, + points: _rectPoints(size: 1), + holes: [_rectPoints(size: 0.5)], + ); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -50,10 +59,10 @@ void main() { testWidgets('Initializing a polygon', (WidgetTester tester) async { final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolygons({p1})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonsToAdd.length, 1); final Polygon initializedPolygon = platformGoogleMap.polygonsToAdd.first; @@ -66,11 +75,11 @@ void main() { final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); final Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1, p2: p2))); + await tester.pumpWidget(_mapWithPolygons({p1})); + await tester.pumpWidget(_mapWithPolygons({p1, p2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonsToAdd.length, 1); final Polygon addedPolygon = platformGoogleMap.polygonsToAdd.first; @@ -84,11 +93,11 @@ void main() { testWidgets("Removing a polygon", (WidgetTester tester) async { final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolygons(null)); + await tester.pumpWidget(_mapWithPolygons({p1})); + await tester.pumpWidget(_mapWithPolygons({})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonIdsToRemove.length, 1); expect(platformGoogleMap.polygonIdsToRemove.first, equals(p1.polygonId)); @@ -101,11 +110,11 @@ void main() { final Polygon p2 = Polygon(polygonId: PolygonId("polygon_1"), geodesic: true); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p2))); + await tester.pumpWidget(_mapWithPolygons({p1})); + await tester.pumpWidget(_mapWithPolygons({p2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonsToChange.length, 1); expect(platformGoogleMap.polygonsToChange.first, equals(p2)); @@ -113,35 +122,18 @@ void main() { expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); - testWidgets("Updating a polygon", (WidgetTester tester) async { - final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - final Polygon p2 = - Polygon(polygonId: PolygonId("polygon_1"), geodesic: true); - - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p2))); - - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; - expect(platformGoogleMap.polygonsToChange.length, 1); - - final Polygon update = platformGoogleMap.polygonsToChange.first; - expect(update, equals(p2)); - expect(update.geodesic, true); - }); - testWidgets("Mutate a polygon", (WidgetTester tester) async { final Polygon p1 = Polygon( polygonId: PolygonId("polygon_1"), points: [const LatLng(0.0, 0.0)], ); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolygons({p1})); p1.points.add(const LatLng(1.0, 1.0)); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolygons({p1})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonsToChange.length, 1); expect(platformGoogleMap.polygonsToChange.first, equals(p1)); @@ -152,16 +144,16 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); - final Set prev = _toSet(p1: p1, p2: p2); + final Set prev = {p1, p2}; p1 = Polygon(polygonId: PolygonId("polygon_1"), visible: false); p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true); - final Set cur = _toSet(p1: p1, p2: p2); + final Set cur = {p1, p2}; await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonsToChange, cur); expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); @@ -171,18 +163,18 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); final Polygon p3 = Polygon(polygonId: PolygonId("polygon_3")); - final Set prev = _toSet(p2: p2, p3: p3); + final Set prev = {p2, p3}; // p1 is added, p2 is updated, p3 is removed. final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true); - final Set cur = _toSet(p1: p1, p2: p2); + final Set cur = {p1, p2}; await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonsToChange.length, 1); expect(platformGoogleMap.polygonsToAdd.length, 1); @@ -197,34 +189,215 @@ void main() { final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); final Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); Polygon p3 = Polygon(polygonId: PolygonId("polygon_3")); - final Set prev = _toSet(p1: p1, p2: p2, p3: p3); + final Set prev = {p1, p2, p3}; p3 = Polygon(polygonId: PolygonId("polygon_3"), geodesic: true); - final Set cur = _toSet(p1: p1, p2: p2, p3: p3); + final Set cur = {p1, p2, p3}; await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToChange, _toSet(p3: p3)); + expect(platformGoogleMap.polygonsToChange, {p3}); expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); testWidgets("Update non platform related attr", (WidgetTester tester) async { Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - final Set prev = _toSet(p1: p1); + final Set prev = {p1}; p1 = Polygon(polygonId: PolygonId("polygon_1"), onTap: () => print(2 + 2)); - final Set cur = _toSet(p1: p1); + final Set cur = {p1}; await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets('Initializing a polygon with points and hole', + (WidgetTester tester) async { + final Polygon p1 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + await tester.pumpWidget(_mapWithPolygons({p1})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.polygonsToAdd.length, 1); + + final Polygon initializedPolygon = platformGoogleMap.polygonsToAdd.first; + expect(initializedPolygon, equals(p1)); + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToChange.isEmpty, true); + }); + + testWidgets("Adding a polygon with points and hole", + (WidgetTester tester) async { + final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + final Polygon p2 = _polygonWithPointsAndHole(PolygonId("polygon_2")); + + await tester.pumpWidget(_mapWithPolygons({p1})); + await tester.pumpWidget(_mapWithPolygons({p1, p2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.polygonsToAdd.length, 1); + + final Polygon addedPolygon = platformGoogleMap.polygonsToAdd.first; + expect(addedPolygon, equals(p2)); + + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); expect(platformGoogleMap.polygonsToChange.isEmpty, true); + }); + + testWidgets("Removing a polygon with points and hole", + (WidgetTester tester) async { + final Polygon p1 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + + await tester.pumpWidget(_mapWithPolygons({p1})); + await tester.pumpWidget(_mapWithPolygons({})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.polygonIdsToRemove.length, 1); + expect(platformGoogleMap.polygonIdsToRemove.first, equals(p1.polygonId)); + + expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets("Updating a polygon by adding points and hole", + (WidgetTester tester) async { + final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + final Polygon p2 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + + await tester.pumpWidget(_mapWithPolygons({p1})); + await tester.pumpWidget(_mapWithPolygons({p2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.polygonsToChange.length, 1); + expect(platformGoogleMap.polygonsToChange.first, equals(p2)); + + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets("Mutate a polygon with points and holes", + (WidgetTester tester) async { + final Polygon p1 = Polygon( + polygonId: PolygonId("polygon_1"), + points: _rectPoints(size: 1), + holes: [_rectPoints(size: 0.5)], + ); + await tester.pumpWidget(_mapWithPolygons({p1})); + + p1.points + ..clear() + ..addAll(_rectPoints(size: 2)); + p1.holes + ..clear() + ..addAll([_rectPoints(size: 1)]); + await tester.pumpWidget(_mapWithPolygons({p1})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.polygonsToChange.length, 1); + expect(platformGoogleMap.polygonsToChange.first, equals(p1)); + + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets("Multi Update polygons with points and hole", + (WidgetTester tester) async { + Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + Polygon p2 = Polygon( + polygonId: PolygonId("polygon_2"), + points: _rectPoints(size: 2), + holes: [_rectPoints(size: 1)], + ); + final Set prev = {p1, p2}; + p1 = Polygon(polygonId: PolygonId("polygon_1"), visible: false); + p2 = p2.copyWith( + pointsParam: _rectPoints(size: 5), + holesParam: [_rectPoints(size: 2)], + ); + final Set cur = {p1, p2}; + + await tester.pumpWidget(_mapWithPolygons(prev)); + await tester.pumpWidget(_mapWithPolygons(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.polygonsToChange, cur); + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets("Multi Update polygons with points and hole", + (WidgetTester tester) async { + Polygon p2 = Polygon( + polygonId: PolygonId("polygon_2"), + points: _rectPoints(size: 2), + holes: [_rectPoints(size: 1)], + ); + final Polygon p3 = Polygon(polygonId: PolygonId("polygon_3")); + final Set prev = {p2, p3}; + + // p1 is added, p2 is updated, p3 is removed. + final Polygon p1 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + p2 = p2.copyWith( + pointsParam: _rectPoints(size: 5), + holesParam: [_rectPoints(size: 3)], + ); + final Set cur = {p1, p2}; + + await tester.pumpWidget(_mapWithPolygons(prev)); + await tester.pumpWidget(_mapWithPolygons(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.polygonsToChange.length, 1); + expect(platformGoogleMap.polygonsToAdd.length, 1); + expect(platformGoogleMap.polygonIdsToRemove.length, 1); + + expect(platformGoogleMap.polygonsToChange.first, equals(p2)); + expect(platformGoogleMap.polygonsToAdd.first, equals(p1)); + expect(platformGoogleMap.polygonIdsToRemove.first, equals(p3.polygonId)); + }); + + testWidgets("Partial Update polygons with points and hole", + (WidgetTester tester) async { + final Polygon p1 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + final Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); + Polygon p3 = Polygon( + polygonId: PolygonId("polygon_3"), + points: _rectPoints(size: 2), + holes: [_rectPoints(size: 1)], + ); + final Set prev = {p1, p2, p3}; + p3 = p3.copyWith( + pointsParam: _rectPoints(size: 5), + holesParam: [_rectPoints(size: 3)], + ); + final Set cur = {p1, p2, p3}; + + await tester.pumpWidget(_mapWithPolygons(prev)); + await tester.pumpWidget(_mapWithPolygons(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.polygonsToChange, {p3}); expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); diff --git a/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart index 269e8f1313f5..3644f83a1adc 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart @@ -9,20 +9,6 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'fake_maps_controllers.dart'; -Set _toSet({Polyline p1, Polyline p2, Polyline p3}) { - final Set res = Set.identity(); - if (p1 != null) { - res.add(p1); - } - if (p2 != null) { - res.add(p2); - } - if (p3 != null) { - res.add(p3); - } - return res; -} - Widget _mapWithPolylines(Set polylines) { return Directionality( textDirection: TextDirection.ltr, @@ -50,10 +36,10 @@ void main() { testWidgets('Initializing a polyline', (WidgetTester tester) async { final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolylines({p1})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToAdd.length, 1); final Polyline initializedPolyline = platformGoogleMap.polylinesToAdd.first; @@ -66,11 +52,11 @@ void main() { final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); final Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1, p2: p2))); + await tester.pumpWidget(_mapWithPolylines({p1})); + await tester.pumpWidget(_mapWithPolylines({p1, p2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToAdd.length, 1); final Polyline addedPolyline = platformGoogleMap.polylinesToAdd.first; @@ -84,11 +70,11 @@ void main() { testWidgets("Removing a polyline", (WidgetTester tester) async { final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolylines(null)); + await tester.pumpWidget(_mapWithPolylines({p1})); + await tester.pumpWidget(_mapWithPolylines({})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylineIdsToRemove.length, 1); expect(platformGoogleMap.polylineIdsToRemove.first, equals(p1.polylineId)); @@ -101,11 +87,11 @@ void main() { final Polyline p2 = Polyline(polylineId: PolylineId("polyline_1"), geodesic: true); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p2))); + await tester.pumpWidget(_mapWithPolylines({p1})); + await tester.pumpWidget(_mapWithPolylines({p2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToChange.length, 1); expect(platformGoogleMap.polylinesToChange.first, equals(p2)); @@ -118,11 +104,11 @@ void main() { final Polyline p2 = Polyline(polylineId: PolylineId("polyline_1"), geodesic: true); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p2))); + await tester.pumpWidget(_mapWithPolylines({p1})); + await tester.pumpWidget(_mapWithPolylines({p2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToChange.length, 1); final Polyline update = platformGoogleMap.polylinesToChange.first; @@ -135,13 +121,13 @@ void main() { polylineId: PolylineId("polyline_1"), points: [const LatLng(0.0, 0.0)], ); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolylines({p1})); p1.points.add(const LatLng(1.0, 1.0)); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolylines({p1})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToChange.length, 1); expect(platformGoogleMap.polylinesToChange.first, equals(p1)); @@ -152,16 +138,16 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); - final Set prev = _toSet(p1: p1, p2: p2); + final Set prev = {p1, p2}; p1 = Polyline(polylineId: PolylineId("polyline_1"), visible: false); p2 = Polyline(polylineId: PolylineId("polyline_2"), geodesic: true); - final Set cur = _toSet(p1: p1, p2: p2); + final Set cur = {p1, p2}; await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToChange, cur); expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); @@ -171,18 +157,18 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); final Polyline p3 = Polyline(polylineId: PolylineId("polyline_3")); - final Set prev = _toSet(p2: p2, p3: p3); + final Set prev = {p2, p3}; // p1 is added, p2 is updated, p3 is removed. final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); p2 = Polyline(polylineId: PolylineId("polyline_2"), geodesic: true); - final Set cur = _toSet(p1: p1, p2: p2); + final Set cur = {p1, p2}; await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToChange.length, 1); expect(platformGoogleMap.polylinesToAdd.length, 1); @@ -197,37 +183,33 @@ void main() { final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); final Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); Polyline p3 = Polyline(polylineId: PolylineId("polyline_3")); - final Set prev = _toSet(p1: p1, p2: p2, p3: p3); + final Set prev = {p1, p2, p3}; p3 = Polyline(polylineId: PolylineId("polyline_3"), geodesic: true); - final Set cur = _toSet(p1: p1, p2: p2, p3: p3); + final Set cur = {p1, p2, p3}; await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylinesToChange, _toSet(p3: p3)); + expect(platformGoogleMap.polylinesToChange, {p3}); expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); expect(platformGoogleMap.polylinesToAdd.isEmpty, true); }); testWidgets("Update non platform related attr", (WidgetTester tester) async { Polyline p1 = Polyline(polylineId: PolylineId("polyline_1"), onTap: null); - final Set prev = _toSet( - p1: p1, - ); + final Set prev = {p1}; p1 = Polyline( polylineId: PolylineId("polyline_1"), onTap: () => print(2 + 2)); - final Set cur = _toSet( - p1: p1, - ); + final Set cur = {p1}; await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToChange.isEmpty, true); expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); diff --git a/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart new file mode 100644 index 000000000000..d2b6efb69e66 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart @@ -0,0 +1,196 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'fake_maps_controllers.dart'; + +Widget _mapWithTileOverlays(Set tileOverlays) { + return Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + tileOverlays: tileOverlays, + ), + ); +} + +void main() { + final FakePlatformViewsController fakePlatformViewsController = + FakePlatformViewsController(); + + setUpAll(() { + SystemChannels.platform_views.setMockMethodCallHandler( + fakePlatformViewsController.fakePlatformViewsMethodHandler); + }); + + setUp(() { + fakePlatformViewsController.reset(); + }); + + testWidgets('Initializing a tile overlay', (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + await tester.pumpWidget(_mapWithTileOverlays({t1})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.tileOverlaysToAdd.length, 1); + + final TileOverlay initializedTileOverlay = + platformGoogleMap.tileOverlaysToAdd.first; + expect(initializedTileOverlay, equals(t1)); + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + }); + + testWidgets("Adding a tile overlay", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + final TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + + await tester.pumpWidget(_mapWithTileOverlays({t1})); + await tester.pumpWidget(_mapWithTileOverlays({t1, t2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.tileOverlaysToAdd.length, 1); + + final TileOverlay addedTileOverlay = + platformGoogleMap.tileOverlaysToAdd.first; + expect(addedTileOverlay, equals(t2)); + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + + expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + }); + + testWidgets("Removing a tile overlay", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + + await tester.pumpWidget(_mapWithTileOverlays({t1})); + await tester.pumpWidget(_mapWithTileOverlays({})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.tileOverlayIdsToRemove.length, 1); + expect(platformGoogleMap.tileOverlayIdsToRemove.first, + equals(t1.tileOverlayId)); + + expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + }); + + testWidgets("Updating a tile overlay", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + final TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"), zIndex: 10); + + await tester.pumpWidget(_mapWithTileOverlays({t1})); + await tester.pumpWidget(_mapWithTileOverlays({t2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.tileOverlaysToChange.length, 1); + expect(platformGoogleMap.tileOverlaysToChange.first, equals(t2)); + + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + }); + + testWidgets("Updating a tile overlay", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + final TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"), zIndex: 10); + + await tester.pumpWidget(_mapWithTileOverlays({t1})); + await tester.pumpWidget(_mapWithTileOverlays({t2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.tileOverlaysToChange.length, 1); + + final TileOverlay update = platformGoogleMap.tileOverlaysToChange.first; + expect(update, equals(t2)); + expect(update.zIndex, 10); + }); + + testWidgets("Multi Update", (WidgetTester tester) async { + TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + final Set prev = {t1, t2}; + t1 = TileOverlay( + tileOverlayId: TileOverlayId("tile_overlay_1"), visible: false); + t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"), zIndex: 10); + final Set cur = {t1, t2}; + + await tester.pumpWidget(_mapWithTileOverlays(prev)); + await tester.pumpWidget(_mapWithTileOverlays(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.tileOverlaysToChange, cur); + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + }); + + testWidgets("Multi Update", (WidgetTester tester) async { + TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + final TileOverlay t3 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3")); + final Set prev = {t2, t3}; + + // t1 is added, t2 is updated, t3 is removed. + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"), zIndex: 10); + final Set cur = {t1, t2}; + + await tester.pumpWidget(_mapWithTileOverlays(prev)); + await tester.pumpWidget(_mapWithTileOverlays(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.tileOverlaysToChange.length, 1); + expect(platformGoogleMap.tileOverlaysToAdd.length, 1); + expect(platformGoogleMap.tileOverlayIdsToRemove.length, 1); + + expect(platformGoogleMap.tileOverlaysToChange.first, equals(t2)); + expect(platformGoogleMap.tileOverlaysToAdd.first, equals(t1)); + expect(platformGoogleMap.tileOverlayIdsToRemove.first, + equals(t3.tileOverlayId)); + }); + + testWidgets("Partial Update", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + final TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + TileOverlay t3 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3")); + final Set prev = {t1, t2, t3}; + t3 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3"), zIndex: 10); + final Set cur = {t1, t2, t3}; + + await tester.pumpWidget(_mapWithTileOverlays(prev)); + await tester.pumpWidget(_mapWithTileOverlays(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.tileOverlaysToChange, {t3}); + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + }); +} 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 dc8eddf8b557..0d5748d13f79 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,28 @@ +## 2.0.0 + +* Migrated to null-safety. +* BREAKING CHANGE: Removed deprecated APIs. +* BREAKING CHANGE: Many sets in APIs that used to treat null and empty set as + equivalent now require passing an empty set. +* BREAKING CHANGE: toJson now always returns an `Object`; the details of the + object type and structure should be treated as an implementation detail. + +## 1.2.0 + +* Add TileOverlay support. + +## 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. + ## 1.0.4 * Add a `dispose` method to the interface, so implementations may cleanup resources acquired on `init`. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index 31392354d946..3d16127ab7a9 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart @@ -12,6 +12,28 @@ import 'package:flutter/gestures.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:stream_transform/stream_transform.dart'; +import '../types/tile_overlay_updates.dart'; +import '../types/utils/tile_overlay.dart'; + +/// Error thrown when an unknown map ID is provided to a method channel API. +class UnknownMapIDError extends Error { + /// Creates an assertion error with the provided [mapId] and optional + /// [message]. + UnknownMapIDError(this.mapId, [this.message]); + + /// The unknown ID. + final int mapId; + + /// Message describing the assertion error. + final Object? message; + + String toString() { + if (message != null) { + return "Unknown map ID $mapId: ${Error.safeToString(message)}"; + } + return "Unknown map ID $mapId"; + } +} /// An implementation of [GoogleMapsFlutterPlatform] that uses [MethodChannel] to communicate with the native code. /// @@ -30,16 +52,20 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { /// Accesses the MethodChannel associated to the passed mapId. MethodChannel channel(int mapId) { - return _channels[mapId]; + MethodChannel? channel = _channels[mapId]; + if (channel == null) { + throw UnknownMapIDError(mapId); + } + return channel; } - /// Initializes the platform interface with [id]. - /// - /// This method is called when the plugin is first initialized. + // Keep a collection of mapId to a map of TileOverlays. + final Map> _tileOverlays = {}; + @override Future init(int mapId) { - MethodChannel channel; - if (!_channels.containsKey(mapId)) { + MethodChannel? channel = _channels[mapId]; + if (channel == null) { channel = MethodChannel('plugins.flutter.io/google_maps_$mapId'); channel.setMethodCallHandler( (MethodCall call) => _handleMethodCall(call, mapId)); @@ -48,9 +74,8 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { return channel.invokeMethod('map#waitForMap'); } - /// Dispose of the native resources. @override - void dispose({int mapId}) { + void dispose({required int mapId}) { // Noop! } @@ -67,57 +92,57 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { _mapEventStreamController.stream.where((event) => event.mapId == mapId); @override - Stream onCameraMoveStarted({@required int mapId}) { + Stream onCameraMoveStarted({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onCameraMove({@required int mapId}) { + Stream onCameraMove({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onCameraIdle({@required int mapId}) { + Stream onCameraIdle({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onMarkerTap({@required int mapId}) { + Stream onMarkerTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onInfoWindowTap({@required int mapId}) { + Stream onInfoWindowTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onMarkerDragEnd({@required int mapId}) { + Stream onMarkerDragEnd({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onPolylineTap({@required int mapId}) { + Stream onPolylineTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onPolygonTap({@required int mapId}) { + Stream onPolygonTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onCircleTap({@required int mapId}) { + Stream onCircleTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onTap({@required int mapId}) { + Stream onTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onLongPress({@required int mapId}) { + Stream onLongPress({required int mapId}) { return _events(mapId).whereType(); } @@ -129,7 +154,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { case 'camera#onMove': _mapEventStreamController.add(CameraMoveEvent( mapId, - CameraPosition.fromMap(call.arguments['position']), + CameraPosition.fromMap(call.arguments['position'])!, )); break; case 'camera#onIdle': @@ -144,7 +169,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { case 'marker#onDragEnd': _mapEventStreamController.add(MarkerDragEndEvent( mapId, - LatLng.fromJson(call.arguments['position']), + LatLng.fromJson(call.arguments['position'])!, MarkerId(call.arguments['markerId']), )); break; @@ -175,30 +200,40 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { case 'map#onTap': _mapEventStreamController.add(MapTapEvent( mapId, - LatLng.fromJson(call.arguments['position']), + LatLng.fromJson(call.arguments['position'])!, )); break; case 'map#onLongPress': _mapEventStreamController.add(MapLongPressEvent( mapId, - LatLng.fromJson(call.arguments['position']), + LatLng.fromJson(call.arguments['position'])!, )); break; + case 'tileOverlay#getTile': + final Map? tileOverlaysForThisMap = + _tileOverlays[mapId]; + final String tileOverlayId = call.arguments['tileOverlayId']; + final TileOverlay? tileOverlay = + tileOverlaysForThisMap?[TileOverlayId(tileOverlayId)]; + TileProvider? tileProvider = tileOverlay?.tileProvider; + if (tileProvider == null) { + return TileProvider.noTile.toJson(); + } + final Tile tile = await tileProvider.getTile( + call.arguments['x'], + call.arguments['y'], + call.arguments['zoom'], + ); + return tile.toJson(); default: throw MissingPluginException(); } } - /// Updates configuration options of the map user interface. - /// - /// Change listeners are notified once the update has been made on the - /// platform side. - /// - /// The returned [Future] completes after listeners have been notified. @override Future updateMapOptions( Map optionsUpdate, { - @required int mapId, + required int mapId, }) { assert(optionsUpdate != null); return channel(mapId).invokeMethod( @@ -209,16 +244,10 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } - /// Updates marker configuration. - /// - /// Change listeners are notified once the update has been made on the - /// platform side. - /// - /// The returned [Future] completes after listeners have been notified. @override Future updateMarkers( MarkerUpdates markerUpdates, { - @required int mapId, + required int mapId, }) { assert(markerUpdates != null); return channel(mapId).invokeMethod( @@ -227,16 +256,10 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } - /// Updates polygon configuration. - /// - /// Change listeners are notified once the update has been made on the - /// platform side. - /// - /// The returned [Future] completes after listeners have been notified. @override Future updatePolygons( PolygonUpdates polygonUpdates, { - @required int mapId, + required int mapId, }) { assert(polygonUpdates != null); return channel(mapId).invokeMethod( @@ -245,16 +268,10 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } - /// Updates polyline configuration. - /// - /// Change listeners are notified once the update has been made on the - /// platform side. - /// - /// The returned [Future] completes after listeners have been notified. @override Future updatePolylines( PolylineUpdates polylineUpdates, { - @required int mapId, + required int mapId, }) { assert(polylineUpdates != null); return channel(mapId).invokeMethod( @@ -263,16 +280,10 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } - /// Updates circle configuration. - /// - /// Change listeners are notified once the update has been made on the - /// platform side. - /// - /// The returned [Future] completes after listeners have been notified. @override Future updateCircles( CircleUpdates circleUpdates, { - @required int mapId, + required int mapId, }) { assert(circleUpdates != null); return channel(mapId).invokeMethod( @@ -281,185 +292,171 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } - /// Starts an animated change of the map camera position. - /// - /// The returned [Future] completes after the change has been started on the - /// platform side. + @override + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) { + final Map? currentTileOverlays = + _tileOverlays[mapId]; + Set previousSet = currentTileOverlays != null + ? currentTileOverlays.values.toSet() + : {}; + final TileOverlayUpdates updates = + TileOverlayUpdates.from(previousSet, newTileOverlays); + _tileOverlays[mapId] = keyTileOverlayId(newTileOverlays); + return channel(mapId).invokeMethod( + 'tileOverlays#update', + updates.toJson(), + ); + } + + @override + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) { + return channel(mapId) + .invokeMethod('tileOverlays#clearTileCache', { + 'tileOverlayId': tileOverlayId.value, + }); + } + @override Future animateCamera( CameraUpdate cameraUpdate, { - @required int mapId, + required int mapId, }) { - return channel(mapId) - .invokeMethod('camera#animate', { + return channel(mapId).invokeMethod('camera#animate', { 'cameraUpdate': cameraUpdate.toJson(), }); } - /// Changes the map camera position. - /// - /// The returned [Future] completes after the change has been made on the - /// platform side. @override Future moveCamera( CameraUpdate cameraUpdate, { - @required int mapId, + required int mapId, }) { return channel(mapId).invokeMethod('camera#move', { 'cameraUpdate': cameraUpdate.toJson(), }); } - /// Sets the styling of the base map. - /// - /// Set to `null` to clear any previous custom styling. - /// - /// If problems were detected with the [mapStyle], including un-parsable - /// styling JSON, unrecognized feature type, unrecognized element type, or - /// invalid styler keys: [MapStyleException] is thrown and the current - /// style is left unchanged. - /// - /// The style string can be generated using [map style tool](https://mapstyle.withgoogle.com/). - /// Also, refer [iOS](https://developers.google.com/maps/documentation/ios-sdk/style-reference) - /// and [Android](https://developers.google.com/maps/documentation/android-sdk/style-reference) - /// style reference for more information regarding the supported styles. @override Future setMapStyle( - String mapStyle, { - @required int mapId, + String? mapStyle, { + required int mapId, }) async { - final List successAndError = await channel(mapId) - .invokeMethod>('map#setStyle', mapStyle); + final List successAndError = (await channel(mapId) + .invokeMethod>('map#setStyle', mapStyle))!; final bool success = successAndError[0]; if (!success) { throw MapStyleException(successAndError[1]); } } - /// Return the region that is visible in a map. @override Future getVisibleRegion({ - @required int mapId, + required int mapId, }) async { - final Map latLngBounds = await channel(mapId) - .invokeMapMethod('map#getVisibleRegion'); - final LatLng southwest = LatLng.fromJson(latLngBounds['southwest']); - final LatLng northeast = LatLng.fromJson(latLngBounds['northeast']); + final Map latLngBounds = (await channel(mapId) + .invokeMapMethod('map#getVisibleRegion'))!; + final LatLng southwest = LatLng.fromJson(latLngBounds['southwest'])!; + final LatLng northeast = LatLng.fromJson(latLngBounds['northeast'])!; return LatLngBounds(northeast: northeast, southwest: southwest); } - /// Return point [Map] of the [screenCoordinateInJson] in the current map view. - /// - /// A projection is used to translate between on screen location and geographic coordinates. - /// Screen location is in screen pixels (not display pixels) with respect to the top left corner - /// of the map, not necessarily of the whole screen. @override Future getScreenCoordinate( LatLng latLng, { - @required int mapId, + required int mapId, }) async { - final Map point = await channel(mapId) + final Map point = (await channel(mapId) .invokeMapMethod( - 'map#getScreenCoordinate', latLng.toJson()); + 'map#getScreenCoordinate', latLng.toJson()))!; - return ScreenCoordinate(x: point['x'], y: point['y']); + return ScreenCoordinate(x: point['x']!, y: point['y']!); } - /// Returns [LatLng] corresponding to the [ScreenCoordinate] in the current map view. - /// - /// Returned [LatLng] corresponds to a screen location. The screen location is specified in screen - /// pixels (not display pixels) relative to the top left of the map, not top left of the whole screen. @override Future getLatLng( ScreenCoordinate screenCoordinate, { - @required int mapId, + required int mapId, }) async { - final List latLng = await channel(mapId) + final List latLng = (await channel(mapId) .invokeMethod>( - 'map#getLatLng', screenCoordinate.toJson()); + 'map#getLatLng', screenCoordinate.toJson()))!; return LatLng(latLng[0], latLng[1]); } - /// Programmatically show the Info Window for a [Marker]. - /// - /// The `markerId` must match one of the markers on the map. - /// An invalid `markerId` triggers an "Invalid markerId" error. - /// - /// * See also: - /// * [hideMarkerInfoWindow] to hide the Info Window. - /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. @override Future showMarkerInfoWindow( MarkerId markerId, { - @required int mapId, + required int mapId, }) { assert(markerId != null); return channel(mapId).invokeMethod( 'markers#showInfoWindow', {'markerId': markerId.value}); } - /// Programmatically hide the Info Window for a [Marker]. - /// - /// The `markerId` must match one of the markers on the map. - /// An invalid `markerId` triggers an "Invalid markerId" error. - /// - /// * See also: - /// * [showMarkerInfoWindow] to show the Info Window. - /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. @override Future hideMarkerInfoWindow( MarkerId markerId, { - @required int mapId, + required int mapId, }) { assert(markerId != null); return channel(mapId).invokeMethod( 'markers#hideInfoWindow', {'markerId': markerId.value}); } - /// Returns `true` when the [InfoWindow] is showing, `false` otherwise. - /// - /// The `markerId` must match one of the markers on the map. - /// An invalid `markerId` triggers an "Invalid markerId" error. - /// - /// * See also: - /// * [showMarkerInfoWindow] to show the Info Window. - /// * [hideMarkerInfoWindow] to hide the Info Window. @override Future isMarkerInfoWindowShown( MarkerId markerId, { - @required int mapId, + required int mapId, }) { assert(markerId != null); return channel(mapId).invokeMethod('markers#isInfoWindowShown', - {'markerId': markerId.value}); + {'markerId': markerId.value}) as Future; } - /// Returns the current zoom level of the map @override Future getZoomLevel({ - @required int mapId, + required int mapId, }) { - return channel(mapId).invokeMethod('map#getZoomLevel'); + return channel(mapId).invokeMethod('map#getZoomLevel') + as Future; } - /// Returns the image bytes of the map @override - Future takeSnapshot({ - @required int mapId, + Future takeSnapshot({ + required int mapId, }) { return channel(mapId).invokeMethod('map#takeSnapshot'); } - /// This method builds the appropriate platform view where the map - /// can be rendered. - /// The `mapId` is passed as a parameter from the framework on the - /// `onPlatformViewCreated` callback. @override Widget buildView( - Map creationParams, - Set> gestureRecognizers, - PlatformViewCreatedCallback onPlatformViewCreated) { + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + final Map creationParams = { + 'initialCameraPosition': initialCameraPosition.toMap(), + 'options': mapOptions, + 'markersToAdd': serializeMarkerSet(markers), + 'polygonsToAdd': serializePolygonSet(polygons), + 'polylinesToAdd': serializePolylineSet(polylines), + 'circlesToAdd': serializeCircleSet(circles), + 'tileOverlaysToAdd': serializeTileOverlaySet(tileOverlays), + }; if (defaultTargetPlatform == TargetPlatform.android) { return AndroidView( viewType: 'plugins.flutter.io/google_maps', diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart index a4f487740811..a363fc30f83c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart @@ -58,7 +58,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// The returned [Future] completes after listeners have been notified. Future updateMapOptions( Map optionsUpdate, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('updateMapOptions() has not been implemented.'); } @@ -71,7 +71,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// The returned [Future] completes after listeners have been notified. Future updateMarkers( MarkerUpdates markerUpdates, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('updateMarkers() has not been implemented.'); } @@ -84,7 +84,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// The returned [Future] completes after listeners have been notified. Future updatePolygons( PolygonUpdates polygonUpdates, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('updatePolygons() has not been implemented.'); } @@ -97,7 +97,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// The returned [Future] completes after listeners have been notified. Future updatePolylines( PolylineUpdates polylineUpdates, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('updatePolylines() has not been implemented.'); } @@ -110,18 +110,45 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// The returned [Future] completes after listeners have been notified. Future updateCircles( CircleUpdates circleUpdates, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('updateCircles() has not been implemented.'); } + /// Updates tile overlay configuration. + /// + /// Change listeners are notified once the update has been made on the + /// platform side. + /// + /// The returned [Future] completes after listeners have been notified. + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) { + throw UnimplementedError('updateTileOverlays() has not been implemented.'); + } + + /// Clears the tile cache so that all tiles will be requested again from the + /// [TileProvider]. + /// + /// The current tiles from this tile overlay will also be + /// cleared from the map after calling this method. The Google Maps SDK maintains a small + /// in-memory cache of tiles. If you want to cache tiles for longer, you + /// should implement an on-disk cache. + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) { + throw UnimplementedError('clearTileCache() has not been implemented.'); + } + /// Starts an animated change of the map camera position. /// /// The returned [Future] completes after the change has been started on the /// platform side. Future animateCamera( CameraUpdate cameraUpdate, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('animateCamera() has not been implemented.'); } @@ -132,7 +159,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// platform side. Future moveCamera( CameraUpdate cameraUpdate, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('moveCamera() has not been implemented.'); } @@ -148,15 +175,15 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// /// The style string can be generated using [map style tool](https://mapstyle.withgoogle.com/). Future setMapStyle( - String mapStyle, { - @required int mapId, + String? mapStyle, { + required int mapId, }) { throw UnimplementedError('setMapStyle() has not been implemented.'); } /// Return the region that is visible in a map. Future getVisibleRegion({ - @required int mapId, + required int mapId, }) { throw UnimplementedError('getVisibleRegion() has not been implemented.'); } @@ -168,7 +195,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// of the map, not necessarily of the whole screen. Future getScreenCoordinate( LatLng latLng, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('getScreenCoordinate() has not been implemented.'); } @@ -180,7 +207,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// of the map, not necessarily of the whole screen. Future getLatLng( ScreenCoordinate screenCoordinate, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('getLatLng() has not been implemented.'); } @@ -195,7 +222,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. Future showMarkerInfoWindow( MarkerId markerId, { - @required int mapId, + required int mapId, }) { throw UnimplementedError( 'showMarkerInfoWindow() has not been implemented.'); @@ -211,7 +238,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. Future hideMarkerInfoWindow( MarkerId markerId, { - @required int mapId, + required int mapId, }) { throw UnimplementedError( 'hideMarkerInfoWindow() has not been implemented.'); @@ -227,21 +254,23 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// * [hideMarkerInfoWindow] to hide the Info Window. Future isMarkerInfoWindowShown( MarkerId markerId, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('updateMapOptions() has not been implemented.'); } - /// Returns the current zoom level of the map + /// Returns the current zoom level of the map. Future getZoomLevel({ - @required int mapId, + required int mapId, }) { throw UnimplementedError('getZoomLevel() has not been implemented.'); } - /// Returns the image bytes of the map - Future takeSnapshot({ - @required int mapId, + /// Returns the image bytes of the map. + /// + /// Returns null if a snapshot cannot be created. + Future takeSnapshot({ + required int mapId, }) { throw UnimplementedError('takeSnapshot() has not been implemented.'); } @@ -250,70 +279,81 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { // into the plugin /// The Camera started moving. - Stream onCameraMoveStarted({@required int mapId}) { + Stream onCameraMoveStarted({required int mapId}) { throw UnimplementedError('onCameraMoveStarted() has not been implemented.'); } /// The Camera finished moving to a new [CameraPosition]. - Stream onCameraMove({@required int mapId}) { + Stream onCameraMove({required int mapId}) { throw UnimplementedError('onCameraMove() has not been implemented.'); } /// The Camera is now idle. - Stream onCameraIdle({@required int mapId}) { + Stream onCameraIdle({required int mapId}) { throw UnimplementedError('onCameraMove() has not been implemented.'); } /// A [Marker] has been tapped. - Stream onMarkerTap({@required int mapId}) { + Stream onMarkerTap({required int mapId}) { throw UnimplementedError('onMarkerTap() has not been implemented.'); } /// An [InfoWindow] has been tapped. - Stream onInfoWindowTap({@required int mapId}) { + Stream onInfoWindowTap({required int mapId}) { throw UnimplementedError('onInfoWindowTap() has not been implemented.'); } /// A [Marker] has been dragged to a different [LatLng] position. - Stream onMarkerDragEnd({@required int mapId}) { + Stream onMarkerDragEnd({required int mapId}) { throw UnimplementedError('onMarkerDragEnd() has not been implemented.'); } /// A [Polyline] has been tapped. - Stream onPolylineTap({@required int mapId}) { + Stream onPolylineTap({required int mapId}) { throw UnimplementedError('onPolylineTap() has not been implemented.'); } /// A [Polygon] has been tapped. - Stream onPolygonTap({@required int mapId}) { + Stream onPolygonTap({required int mapId}) { throw UnimplementedError('onPolygonTap() has not been implemented.'); } /// A [Circle] has been tapped. - Stream onCircleTap({@required int mapId}) { + Stream onCircleTap({required int mapId}) { throw UnimplementedError('onCircleTap() has not been implemented.'); } /// A Map has been tapped at a certain [LatLng]. - Stream onTap({@required int mapId}) { + Stream onTap({required int mapId}) { throw UnimplementedError('onTap() has not been implemented.'); } /// A Map has been long-pressed at a certain [LatLng]. - Stream onLongPress({@required int mapId}) { + Stream onLongPress({required int mapId}) { throw UnimplementedError('onLongPress() has not been implemented.'); } /// Dispose of whatever resources the `mapId` is holding on to. - void dispose({@required int mapId}) { + void dispose({required int mapId}) { throw UnimplementedError('dispose() has not been implemented.'); } /// Returns a widget displaying the map view Widget buildView( - Map creationParams, - Set> gestureRecognizers, - PlatformViewCreatedCallback onPlatformViewCreated) { + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers = + const >{}, + // TODO: Replace with a structured type that's part of the interface. + // See https://github.com/flutter/flutter/issues/70330. + Map mapOptions = const {}, + }) { throw UnimplementedError('buildView() has not been implemented.'); } } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart index a6fdcc1b7e33..cc9887512d0e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart @@ -4,6 +4,7 @@ import 'dart:async' show Future; import 'dart:typed_data' show Uint8List; +import 'dart:ui' show Size; import 'package:flutter/material.dart' show ImageConfiguration, AssetImage, AssetBundleImageKey; @@ -17,6 +18,18 @@ import 'package:flutter/foundation.dart' show kIsWeb; class BitmapDescriptor { const BitmapDescriptor._(this._json); + static const String _defaultMarker = 'defaultMarker'; + static const String _fromAsset = 'fromAsset'; + static const String _fromAssetImage = 'fromAssetImage'; + static const String _fromBytes = 'fromBytes'; + + static const Set _validTypes = { + _defaultMarker, + _fromAsset, + _fromAssetImage, + _fromBytes, + }; + /// Convenience hue value representing red. static const double hueRed = 0.0; @@ -49,28 +62,14 @@ class BitmapDescriptor { /// Creates a BitmapDescriptor that refers to the default marker image. static const BitmapDescriptor defaultMarker = - BitmapDescriptor._(['defaultMarker']); + BitmapDescriptor._([_defaultMarker]); /// Creates a BitmapDescriptor that refers to a colorization of the default /// marker image. For convenience, there is a predefined set of hue values. /// See e.g. [hueYellow]. static BitmapDescriptor defaultMarkerWithHue(double hue) { assert(0.0 <= hue && hue < 360.0); - return BitmapDescriptor._(['defaultMarker', hue]); - } - - /// Creates a BitmapDescriptor using the name of a bitmap image in the assets - /// directory. - /// - /// Use [fromAssetImage]. This method does not respect the screen dpi when - /// picking an asset image. - @Deprecated("Use fromAssetImage instead") - static BitmapDescriptor fromAsset(String assetName, {String package}) { - if (package == null) { - return BitmapDescriptor._(['fromAsset', assetName]); - } else { - return BitmapDescriptor._(['fromAsset', assetName, package]); - } + return BitmapDescriptor._([_defaultMarker, hue]); } /// Creates a [BitmapDescriptor] from an asset image. @@ -83,29 +82,31 @@ class BitmapDescriptor { static Future fromAssetImage( ImageConfiguration configuration, String assetName, { - AssetBundle bundle, - String package, + AssetBundle? bundle, + String? package, bool mipmaps = true, }) async { - if (!mipmaps && configuration.devicePixelRatio != null) { - return BitmapDescriptor._([ - 'fromAssetImage', + double? devicePixelRatio = configuration.devicePixelRatio; + if (!mipmaps && devicePixelRatio != null) { + return BitmapDescriptor._([ + _fromAssetImage, assetName, - configuration.devicePixelRatio, + devicePixelRatio, ]); } final AssetImage assetImage = AssetImage(assetName, package: package, bundle: bundle); final AssetBundleImageKey assetBundleImageKey = await assetImage.obtainKey(configuration); - return BitmapDescriptor._([ - 'fromAssetImage', + final Size? size = configuration.size; + return BitmapDescriptor._([ + _fromAssetImage, assetBundleImageKey.name, assetBundleImageKey.scale, - if (kIsWeb && configuration?.size != null) + if (kIsWeb && size != null) [ - configuration.size.width, - configuration.size.height, + size.width, + size.height, ], ]); } @@ -113,11 +114,56 @@ class BitmapDescriptor { /// Creates a BitmapDescriptor using an array of bytes that must be encoded /// as PNG. static BitmapDescriptor fromBytes(Uint8List byteData) { - return BitmapDescriptor._(['fromBytes', byteData]); + return BitmapDescriptor._([_fromBytes, byteData]); + } + + /// The inverse of .toJson. + // This is needed in Web to re-hydrate BitmapDescriptors that have been + // transformed to JSON for transport. + // TODO(https://github.com/flutter/flutter/issues/70330): Clean this up. + BitmapDescriptor.fromJson(Object json) : _json = json { + assert(_json is List); + final jsonList = json as List; + assert(_validTypes.contains(jsonList[0])); + switch (jsonList[0]) { + case _defaultMarker: + assert(jsonList.length <= 2); + if (jsonList.length == 2) { + assert(jsonList[1] is num); + assert(0 <= jsonList[1] && jsonList[1] < 360); + } + break; + case _fromBytes: + assert(jsonList.length == 2); + assert(jsonList[1] != null && jsonList[1] is List); + assert((jsonList[1] as List).isNotEmpty); + break; + case _fromAsset: + assert(jsonList.length <= 3); + assert(jsonList[1] != null && jsonList[1] is String); + assert((jsonList[1] as String).isNotEmpty); + if (jsonList.length == 3) { + assert(jsonList[2] != null && jsonList[2] is String); + assert((jsonList[2] as String).isNotEmpty); + } + break; + case _fromAssetImage: + assert(jsonList.length <= 4); + assert(jsonList[1] != null && jsonList[1] is String); + assert((jsonList[1] as String).isNotEmpty); + assert(jsonList[2] != null && jsonList[2] is double); + if (jsonList.length == 4) { + assert(jsonList[3] != null && jsonList[3] is List); + assert((jsonList[3] as List).length == 2); + } + break; + default: + break; + } } - final dynamic _json; + final Object _json; /// Convert the object to a Json format. - dynamic toJson() => _json; + Object toJson() => _json; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart index 10ea1e98846a..28acf35962b6 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart @@ -4,8 +4,6 @@ import 'dart:ui' show hashValues, Offset; -import 'package:meta/meta.dart' show required; - import 'types.dart'; /// The position of the map "camera", the view point from which the world is shown in the map view. @@ -19,7 +17,7 @@ class CameraPosition { /// null. const CameraPosition({ this.bearing = 0.0, - @required this.target, + required this.target, this.tilt = 0.0, this.zoom = 0.0, }) : assert(bearing != null), @@ -63,7 +61,7 @@ class CameraPosition { /// Serializes [CameraPosition]. /// /// Mainly for internal use when calling [CameraUpdate.newCameraPosition]. - dynamic toMap() => { + Object toMap() => { 'bearing': bearing, 'target': target.toJson(), 'tilt': tilt, @@ -73,23 +71,27 @@ class CameraPosition { /// Deserializes [CameraPosition] from a map. /// /// Mainly for internal use. - static CameraPosition fromMap(dynamic json) { - if (json == null) { + static CameraPosition? fromMap(Object? json) { + if (json == null || !(json is Map)) { + return null; + } + final LatLng? target = LatLng.fromJson(json['target']); + if (target == null) { return null; } return CameraPosition( bearing: json['bearing'], - target: LatLng.fromJson(json['target']), + target: target, tilt: json['tilt'], zoom: json['zoom'], ); } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (identical(this, other)) return true; if (runtimeType != other.runtimeType) return false; - final CameraPosition typedOther = other; + final CameraPosition typedOther = other as CameraPosition; return bearing == typedOther.bearing && target == typedOther.target && tilt == typedOther.tilt && @@ -112,14 +114,14 @@ class CameraUpdate { /// Returns a camera update that moves the camera to the specified position. static CameraUpdate newCameraPosition(CameraPosition cameraPosition) { return CameraUpdate._( - ['newCameraPosition', cameraPosition.toMap()], + ['newCameraPosition', cameraPosition.toMap()], ); } /// Returns a camera update that moves the camera target to the specified /// geographical location. static CameraUpdate newLatLng(LatLng latLng) { - return CameraUpdate._(['newLatLng', latLng.toJson()]); + return CameraUpdate._(['newLatLng', latLng.toJson()]); } /// Returns a camera update that transforms the camera so that the specified @@ -127,7 +129,7 @@ class CameraUpdate { /// possible zoom level. A non-zero [padding] insets the bounding box from the /// map view's edges. The camera's new tilt and bearing will both be 0.0. static CameraUpdate newLatLngBounds(LatLngBounds bounds, double padding) { - return CameraUpdate._([ + return CameraUpdate._([ 'newLatLngBounds', bounds.toJson(), padding, @@ -138,7 +140,7 @@ class CameraUpdate { /// geographical location and zoom level. static CameraUpdate newLatLngZoom(LatLng latLng, double zoom) { return CameraUpdate._( - ['newLatLngZoom', latLng.toJson(), zoom], + ['newLatLngZoom', latLng.toJson(), zoom], ); } @@ -150,18 +152,18 @@ class CameraUpdate { /// 75 to the south of the current location, measured in screen coordinates. static CameraUpdate scrollBy(double dx, double dy) { return CameraUpdate._( - ['scrollBy', dx, dy], + ['scrollBy', dx, dy], ); } /// Returns a camera update that modifies the camera zoom level by the /// specified amount. The optional [focus] is a screen point whose underlying /// geographical location should be invariant, if possible, by the movement. - static CameraUpdate zoomBy(double amount, [Offset focus]) { + static CameraUpdate zoomBy(double amount, [Offset? focus]) { if (focus == null) { - return CameraUpdate._(['zoomBy', amount]); + return CameraUpdate._(['zoomBy', amount]); } else { - return CameraUpdate._([ + return CameraUpdate._([ 'zoomBy', amount, [focus.dx, focus.dy], @@ -174,7 +176,7 @@ class CameraUpdate { /// /// Equivalent to the result of calling `zoomBy(1.0)`. static CameraUpdate zoomIn() { - return CameraUpdate._(['zoomIn']); + return CameraUpdate._(['zoomIn']); } /// Returns a camera update that zooms the camera out, bringing the camera @@ -182,16 +184,16 @@ class CameraUpdate { /// /// Equivalent to the result of calling `zoomBy(-1.0)`. static CameraUpdate zoomOut() { - return CameraUpdate._(['zoomOut']); + return CameraUpdate._(['zoomOut']); } /// Returns a camera update that sets the camera zoom level. static CameraUpdate zoomTo(double zoom) { - return CameraUpdate._(['zoomTo', zoom]); + return CameraUpdate._(['zoomTo', zoom]); } - final dynamic _json; + final Object _json; /// Converts this object to something serializable in JSON. - dynamic toJson() => _json; + Object toJson() => _json; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart index 68bf14c36408..c88923a59404 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart @@ -17,16 +17,16 @@ class Cap { /// /// This is the default cap type at start and end vertices of Polylines with /// solid stroke pattern. - static const Cap buttCap = Cap._(['buttCap']); + static const Cap buttCap = Cap._(['buttCap']); /// Cap that is a semicircle with radius equal to half the stroke width, /// centered at the start or end vertex of a [Polyline] with solid stroke /// pattern. - static const Cap roundCap = Cap._(['roundCap']); + static const Cap roundCap = Cap._(['roundCap']); /// Cap that is squared off after extending half the stroke width beyond the /// start or end vertex of a [Polyline] with solid stroke pattern. - static const Cap squareCap = Cap._(['squareCap']); + static const Cap squareCap = Cap._(['squareCap']); /// Constructs a new CustomCap with a bitmap overlay centered at the start or /// end vertex of a [Polyline], orientated according to the direction of the line's @@ -45,11 +45,11 @@ class Cap { }) { assert(bitmapDescriptor != null); assert(refWidth > 0.0); - return Cap._(['customCap', bitmapDescriptor.toJson(), refWidth]); + return Cap._(['customCap', bitmapDescriptor.toJson(), refWidth]); } - final dynamic _json; + final Object _json; /// Converts this object to something serializable in JSON. - dynamic toJson() => _json; + Object toJson() => _json; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart index d1418a4c30b1..e3198dfd6512 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart' show VoidCallback; import 'package:flutter/material.dart' show Color, Colors; -import 'package:meta/meta.dart' show immutable, required; +import 'package:meta/meta.dart' show immutable; import 'types.dart'; @@ -12,36 +12,17 @@ import 'types.dart'; /// /// This does not have to be globally unique, only unique among the list. @immutable -class CircleId { +class CircleId extends MapsObjectId { /// Creates an immutable identifier for a [Circle]. - CircleId(this.value) : assert(value != null); - - /// value of the [CircleId]. - final String value; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final CircleId typedOther = other; - return value == typedOther.value; - } - - @override - int get hashCode => value.hashCode; - - @override - String toString() { - return 'CircleId{value: $value}'; - } + CircleId(String value) : super(value); } /// Draws a circle on the map. @immutable -class Circle { +class Circle implements MapsObject { /// Creates an immutable representation of a [Circle] to draw on [GoogleMap]. const Circle({ - @required this.circleId, + required this.circleId, this.consumeTapEvents = false, this.fillColor = Colors.transparent, this.center = const LatLng(0.0, 0.0), @@ -56,6 +37,9 @@ class Circle { /// Uniquely identifies a [Circle]. final CircleId circleId; + @override + CircleId get mapsId => circleId; + /// True if the [Circle] consumes tap events. /// /// If this is false, [onTap] callback will not be triggered. @@ -91,20 +75,20 @@ class Circle { final int zIndex; /// Callbacks to receive tap events for circle placed on this map. - final VoidCallback onTap; + final VoidCallback? onTap; /// Creates a new [Circle] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Circle copyWith({ - bool consumeTapEventsParam, - Color fillColorParam, - LatLng centerParam, - double radiusParam, - Color strokeColorParam, - int strokeWidthParam, - bool visibleParam, - int zIndexParam, - VoidCallback onTapParam, + bool? consumeTapEventsParam, + Color? fillColorParam, + LatLng? centerParam, + double? radiusParam, + Color? strokeColorParam, + int? strokeWidthParam, + bool? visibleParam, + int? zIndexParam, + VoidCallback? onTapParam, }) { return Circle( circleId: circleId, @@ -124,10 +108,10 @@ class Circle { Circle clone() => copyWith(); /// Converts this object to something serializable in JSON. - dynamic toJson() { - final Map json = {}; + Object toJson() { + final Map json = {}; - void addIfPresent(String fieldName, dynamic value) { + void addIfPresent(String fieldName, Object? value) { if (value != null) { json[fieldName] = value; } @@ -150,7 +134,7 @@ class Circle { bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; - final Circle typedOther = other; + final Circle typedOther = other as Circle; return circleId == typedOther.circleId && consumeTapEvents == typedOther.consumeTapEvents && fillColor == typedOther.fillColor && diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart index 6f494423a38f..a0b064b7be2a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart @@ -2,109 +2,23 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - -import 'package:flutter/foundation.dart' show setEquals; - import 'types.dart'; -import 'utils/circle.dart'; /// [Circle] update events to be applied to the [GoogleMap]. /// /// Used in [GoogleMapController] when the map is updated. // (Do not re-export) -class CircleUpdates { +class CircleUpdates extends MapsObjectUpdates { /// Computes [CircleUpdates] given previous and current [Circle]s. - CircleUpdates.from(Set previous, Set current) { - if (previous == null) { - previous = Set.identity(); - } - - if (current == null) { - current = Set.identity(); - } - - final Map previousCircles = keyByCircleId(previous); - final Map currentCircles = keyByCircleId(current); - - final Set prevCircleIds = previousCircles.keys.toSet(); - final Set currentCircleIds = currentCircles.keys.toSet(); - - Circle idToCurrentCircle(CircleId id) { - return currentCircles[id]; - } - - final Set _circleIdsToRemove = - prevCircleIds.difference(currentCircleIds); - - final Set _circlesToAdd = currentCircleIds - .difference(prevCircleIds) - .map(idToCurrentCircle) - .toSet(); - - /// Returns `true` if [current] is not equals to previous one with the - /// same id. - bool hasChanged(Circle current) { - final Circle previous = previousCircles[current.circleId]; - return current != previous; - } - - final Set _circlesToChange = currentCircleIds - .intersection(prevCircleIds) - .map(idToCurrentCircle) - .where(hasChanged) - .toSet(); - - circlesToAdd = _circlesToAdd; - circleIdsToRemove = _circleIdsToRemove; - circlesToChange = _circlesToChange; - } + CircleUpdates.from(Set previous, Set current) + : super.from(previous, current, objectName: 'circle'); /// Set of Circles to be added in this update. - Set circlesToAdd; + Set get circlesToAdd => objectsToAdd; /// Set of CircleIds to be removed in this update. - Set circleIdsToRemove; + Set get circleIdsToRemove => objectIdsToRemove.cast(); /// Set of Circles to be changed in this update. - Set circlesToChange; - - /// Converts this object to something serializable in JSON. - Map toJson() { - final Map updateMap = {}; - - void addIfNonNull(String fieldName, dynamic value) { - if (value != null) { - updateMap[fieldName] = value; - } - } - - addIfNonNull('circlesToAdd', serializeCircleSet(circlesToAdd)); - addIfNonNull('circlesToChange', serializeCircleSet(circlesToChange)); - addIfNonNull('circleIdsToRemove', - circleIdsToRemove.map((CircleId m) => m.value).toList()); - - return updateMap; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final CircleUpdates typedOther = other; - return setEquals(circlesToAdd, typedOther.circlesToAdd) && - setEquals(circleIdsToRemove, typedOther.circleIdsToRemove) && - setEquals(circlesToChange, typedOther.circlesToChange); - } - - @override - int get hashCode => - hashValues(circlesToAdd, circleIdsToRemove, circlesToChange); - - @override - String toString() { - return '_CircleUpdates{circlesToAdd: $circlesToAdd, ' - 'circleIdsToRemove: $circleIdsToRemove, ' - 'circlesToChange: $circlesToChange}'; - } + Set get circlesToChange => objectsToChange; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart index 6b76a6d496ac..a719f0bc741f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart @@ -29,16 +29,18 @@ class LatLng { final double longitude; /// Converts this object to something serializable in JSON. - dynamic toJson() { + Object toJson() { return [latitude, longitude]; } /// Initialize a LatLng from an \[lat, lng\] array. - static LatLng fromJson(dynamic json) { + static LatLng? fromJson(Object? json) { if (json == null) { return null; } - return LatLng(json[0], json[1]); + assert(json is List && json.length == 2); + final list = json as List; + return LatLng(list[0], list[1]); } @override @@ -66,7 +68,7 @@ class LatLngBounds { /// /// The latitude of the southwest corner cannot be larger than the /// latitude of the northeast corner. - LatLngBounds({@required this.southwest, @required this.northeast}) + LatLngBounds({required this.southwest, required this.northeast}) : assert(southwest != null), assert(northeast != null), assert(southwest.latitude <= northeast.latitude); @@ -78,8 +80,8 @@ class LatLngBounds { final LatLng northeast; /// Converts this object to something serializable in JSON. - dynamic toJson() { - return [southwest.toJson(), northeast.toJson()]; + Object toJson() { + return [southwest.toJson(), northeast.toJson()]; } /// Returns whether this rectangle contains the given [LatLng]. @@ -102,13 +104,15 @@ class LatLngBounds { /// Converts a list to [LatLngBounds]. @visibleForTesting - static LatLngBounds fromList(dynamic json) { + static LatLngBounds? fromList(Object? json) { if (json == null) { return null; } + assert(json is List && json.length == 2); + final list = json as List; return LatLngBounds( - southwest: LatLng.fromJson(json[0]), - northeast: LatLng.fromJson(json[1]), + southwest: LatLng.fromJson(list[0])!, + northeast: LatLng.fromJson(list[1])!, ); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart new file mode 100644 index 000000000000..545d46272215 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart @@ -0,0 +1,49 @@ +// 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/foundation.dart' show objectRuntimeType; +import 'package:meta/meta.dart' show immutable; + +/// Uniquely identifies object an among [GoogleMap] collections of a specific +/// type. +/// +/// This does not have to be globally unique, only unique among the collection. +@immutable +class MapsObjectId { + /// Creates an immutable object representing a [T] among [GoogleMap] Ts. + /// + /// An [AssertionError] will be thrown if [value] is null. + MapsObjectId(this.value) : assert(value != null); + + /// The value of the id. + final String value; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final MapsObjectId typedOther = other as MapsObjectId; + return value == typedOther.value; + } + + @override + int get hashCode => value.hashCode; + + @override + String toString() { + return '${objectRuntimeType(this, 'MapsObjectId')}($value)'; + } +} + +/// A common interface for maps types. +abstract class MapsObject { + /// A identifier for this object. + MapsObjectId get mapsId; + + /// Returns a duplicate of this object. + T clone(); + + /// Converts this object to something serializable in JSON. + Object toJson(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart new file mode 100644 index 000000000000..01cf967f54e3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart @@ -0,0 +1,126 @@ +// 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:ui' show hashValues, hashList; + +import 'package:flutter/foundation.dart' show objectRuntimeType, setEquals; + +import 'maps_object.dart'; +import 'utils/maps_object.dart'; + +/// Update specification for a set of objects. +class MapsObjectUpdates { + /// Computes updates given previous and current object sets. + /// + /// [objectName] is the prefix to use when serializing the updates into a JSON + /// dictionary. E.g., 'circle' will give 'circlesToAdd', 'circlesToUpdate', + /// 'circleIdsToRemove'. + MapsObjectUpdates.from( + Set previous, + Set current, { + required this.objectName, + }) { + final Map, T> previousObjects = keyByMapsObjectId(previous); + final Map, T> currentObjects = keyByMapsObjectId(current); + + final Set> previousObjectIds = previousObjects.keys.toSet(); + final Set> currentObjectIds = currentObjects.keys.toSet(); + + /// Maps an ID back to a [T] in [currentObjects]. + /// + /// It is a programming error to call this with an ID that is not guaranteed + /// to be in [currentObjects]. + T _idToCurrentObject(MapsObjectId id) { + return currentObjects[id]!; + } + + _objectIdsToRemove = previousObjectIds.difference(currentObjectIds); + + _objectsToAdd = currentObjectIds + .difference(previousObjectIds) + .map(_idToCurrentObject) + .toSet(); + + // Returns `true` if [current] is not equals to previous one with the + // same id. + bool hasChanged(T current) { + final T? previous = previousObjects[current.mapsId as MapsObjectId]; + return current != previous; + } + + _objectsToChange = currentObjectIds + .intersection(previousObjectIds) + .map(_idToCurrentObject) + .where(hasChanged) + .toSet(); + } + + /// The name of the objects being updated, for use in serialization. + final String objectName; + + /// Set of objects to be added in this update. + Set get objectsToAdd { + return _objectsToAdd; + } + + late Set _objectsToAdd; + + /// Set of objects to be removed in this update. + Set> get objectIdsToRemove { + return _objectIdsToRemove; + } + + late Set> _objectIdsToRemove; + + /// Set of objects to be changed in this update. + Set get objectsToChange { + return _objectsToChange; + } + + late Set _objectsToChange; + + /// Converts this object to JSON. + Object toJson() { + final Map updateMap = {}; + + void addIfNonNull(String fieldName, Object? value) { + if (value != null) { + updateMap[fieldName] = value; + } + } + + addIfNonNull('${objectName}sToAdd', serializeMapsObjectSet(_objectsToAdd)); + addIfNonNull( + '${objectName}sToChange', serializeMapsObjectSet(_objectsToChange)); + addIfNonNull( + '${objectName}IdsToRemove', + _objectIdsToRemove + .map((MapsObjectId m) => m.value) + .toList()); + + return updateMap; + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is MapsObjectUpdates && + setEquals(_objectsToAdd, other._objectsToAdd) && + setEquals(_objectIdsToRemove, other._objectIdsToRemove) && + setEquals(_objectsToChange, other._objectsToChange); + } + + @override + int get hashCode => hashValues(hashList(_objectsToAdd), + hashList(_objectIdsToRemove), hashList(_objectsToChange)); + + @override + String toString() { + return '${objectRuntimeType(this, 'MapsObjectUpdates')}(add: $objectsToAdd, ' + 'remove: $objectIdsToRemove, ' + 'change: $objectsToChange)'; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart index 9b57f9676334..15351d58168b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart @@ -5,15 +5,12 @@ import 'dart:ui' show hashValues, Offset; import 'package:flutter/foundation.dart' show ValueChanged, VoidCallback; -import 'package:meta/meta.dart' show immutable, required; +import 'package:meta/meta.dart' show immutable; import 'types.dart'; -dynamic _offsetToJson(Offset offset) { - if (offset == null) { - return null; - } - return [offset.dx, offset.dy]; +Object _offsetToJson(Offset offset) { + return [offset.dx, offset.dy]; } /// Text labels for a [Marker] info window. @@ -32,12 +29,12 @@ class InfoWindow { /// Text displayed in an info window when the user taps the marker. /// /// A null value means no title. - final String title; + final String? title; /// Additional text displayed below the [title]. /// /// A null value means no additional text. - final String snippet; + final String? snippet; /// The icon image point that will be the anchor of the info window when /// displayed. @@ -48,15 +45,15 @@ class InfoWindow { final Offset anchor; /// onTap callback for this [InfoWindow]. - final VoidCallback onTap; + final VoidCallback? onTap; /// Creates a new [InfoWindow] object whose values are the same as this instance, /// unless overwritten by the specified parameters. InfoWindow copyWith({ - String titleParam, - String snippetParam, - Offset anchorParam, - VoidCallback onTapParam, + String? titleParam, + String? snippetParam, + Offset? anchorParam, + VoidCallback? onTapParam, }) { return InfoWindow( title: titleParam ?? title, @@ -66,10 +63,10 @@ class InfoWindow { ); } - dynamic _toJson() { - final Map json = {}; + Object _toJson() { + final Map json = {}; - void addIfPresent(String fieldName, dynamic value) { + void addIfPresent(String fieldName, Object? value) { if (value != null) { json[fieldName] = value; } @@ -86,7 +83,7 @@ class InfoWindow { bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; - final InfoWindow typedOther = other; + final InfoWindow typedOther = other as InfoWindow; return title == typedOther.title && snippet == typedOther.snippet && anchor == typedOther.anchor; @@ -105,28 +102,9 @@ class InfoWindow { /// /// This does not have to be globally unique, only unique among the list. @immutable -class MarkerId { +class MarkerId extends MapsObjectId { /// Creates an immutable identifier for a [Marker]. - MarkerId(this.value) : assert(value != null); - - /// value of the [MarkerId]. - final String value; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final MarkerId typedOther = other; - return value == typedOther.value; - } - - @override - int get hashCode => value.hashCode; - - @override - String toString() { - return 'MarkerId{value: $value}'; - } + MarkerId(String value) : super(value); } /// Marks a geographical location on the map. @@ -135,7 +113,7 @@ class MarkerId { /// the map's surface; that is, it will not necessarily change orientation /// due to map rotations, tilting, or zooming. @immutable -class Marker { +class Marker implements MapsObject { /// Creates a set of marker configuration options. /// /// Default marker options. @@ -156,7 +134,7 @@ class Marker { /// * reports [onTap] events /// * reports [onDragEnd] events const Marker({ - @required this.markerId, + required this.markerId, this.alpha = 1.0, this.anchor = const Offset(0.5, 1.0), this.consumeTapEvents = false, @@ -175,6 +153,9 @@ class Marker { /// Uniquely identifies a [Marker]. final MarkerId markerId; + @override + MarkerId get mapsId => markerId; + /// The opacity of the marker, between 0.0 and 1.0 inclusive. /// /// 0.0 means fully transparent, 1.0 means fully opaque. @@ -224,27 +205,27 @@ class Marker { final double zIndex; /// Callbacks to receive tap events for markers placed on this map. - final VoidCallback onTap; + final VoidCallback? onTap; /// Signature reporting the new [LatLng] at the end of a drag event. - final ValueChanged onDragEnd; + final ValueChanged? onDragEnd; /// Creates a new [Marker] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Marker copyWith({ - double alphaParam, - Offset anchorParam, - bool consumeTapEventsParam, - bool draggableParam, - bool flatParam, - BitmapDescriptor iconParam, - InfoWindow infoWindowParam, - LatLng positionParam, - double rotationParam, - bool visibleParam, - double zIndexParam, - VoidCallback onTapParam, - ValueChanged onDragEndParam, + double? alphaParam, + Offset? anchorParam, + bool? consumeTapEventsParam, + bool? draggableParam, + bool? flatParam, + BitmapDescriptor? iconParam, + InfoWindow? infoWindowParam, + LatLng? positionParam, + double? rotationParam, + bool? visibleParam, + double? zIndexParam, + VoidCallback? onTapParam, + ValueChanged? onDragEndParam, }) { return Marker( markerId: markerId, @@ -268,10 +249,10 @@ class Marker { Marker clone() => copyWith(); /// Converts this object to something serializable in JSON. - Map toJson() { - final Map json = {}; + Object toJson() { + final Map json = {}; - void addIfPresent(String fieldName, dynamic value) { + void addIfPresent(String fieldName, Object? value) { if (value != null) { json[fieldName] = value; } @@ -283,9 +264,9 @@ class Marker { addIfPresent('consumeTapEvents', consumeTapEvents); addIfPresent('draggable', draggable); addIfPresent('flat', flat); - addIfPresent('icon', icon?.toJson()); - addIfPresent('infoWindow', infoWindow?._toJson()); - addIfPresent('position', position?.toJson()); + addIfPresent('icon', icon.toJson()); + addIfPresent('infoWindow', infoWindow._toJson()); + addIfPresent('position', position.toJson()); addIfPresent('rotation', rotation); addIfPresent('visible', visible); addIfPresent('zIndex', zIndex); @@ -296,7 +277,7 @@ class Marker { bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; - final Marker typedOther = other; + final Marker typedOther = other as Marker; return markerId == typedOther.markerId && alpha == typedOther.alpha && anchor == typedOther.anchor && diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart index bb6ea8813ea3..9c96ab63af18 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart @@ -2,109 +2,23 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - -import 'package:flutter/foundation.dart' show setEquals; - import 'types.dart'; -import 'utils/marker.dart'; /// [Marker] update events to be applied to the [GoogleMap]. /// /// Used in [GoogleMapController] when the map is updated. // (Do not re-export) -class MarkerUpdates { +class MarkerUpdates extends MapsObjectUpdates { /// Computes [MarkerUpdates] given previous and current [Marker]s. - MarkerUpdates.from(Set previous, Set current) { - if (previous == null) { - previous = Set.identity(); - } - - if (current == null) { - current = Set.identity(); - } - - final Map previousMarkers = keyByMarkerId(previous); - final Map currentMarkers = keyByMarkerId(current); - - final Set prevMarkerIds = previousMarkers.keys.toSet(); - final Set currentMarkerIds = currentMarkers.keys.toSet(); - - Marker idToCurrentMarker(MarkerId id) { - return currentMarkers[id]; - } - - final Set _markerIdsToRemove = - prevMarkerIds.difference(currentMarkerIds); - - final Set _markersToAdd = currentMarkerIds - .difference(prevMarkerIds) - .map(idToCurrentMarker) - .toSet(); - - /// Returns `true` if [current] is not equals to previous one with the - /// same id. - bool hasChanged(Marker current) { - final Marker previous = previousMarkers[current.markerId]; - return current != previous; - } - - final Set _markersToChange = currentMarkerIds - .intersection(prevMarkerIds) - .map(idToCurrentMarker) - .where(hasChanged) - .toSet(); - - markersToAdd = _markersToAdd; - markerIdsToRemove = _markerIdsToRemove; - markersToChange = _markersToChange; - } + MarkerUpdates.from(Set previous, Set current) + : super.from(previous, current, objectName: 'marker'); /// Set of Markers to be added in this update. - Set markersToAdd; + Set get markersToAdd => objectsToAdd; /// Set of MarkerIds to be removed in this update. - Set markerIdsToRemove; + Set get markerIdsToRemove => objectIdsToRemove.cast(); /// Set of Markers to be changed in this update. - Set markersToChange; - - /// Converts this object to something serializable in JSON. - Map toJson() { - final Map updateMap = {}; - - void addIfNonNull(String fieldName, dynamic value) { - if (value != null) { - updateMap[fieldName] = value; - } - } - - addIfNonNull('markersToAdd', serializeMarkerSet(markersToAdd)); - addIfNonNull('markersToChange', serializeMarkerSet(markersToChange)); - addIfNonNull('markerIdsToRemove', - markerIdsToRemove.map((MarkerId m) => m.value).toList()); - - return updateMap; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final MarkerUpdates typedOther = other; - return setEquals(markersToAdd, typedOther.markersToAdd) && - setEquals(markerIdsToRemove, typedOther.markerIdsToRemove) && - setEquals(markersToChange, typedOther.markersToChange); - } - - @override - int get hashCode => - hashValues(markersToAdd, markerIdsToRemove, markersToChange); - - @override - String toString() { - return '_MarkerUpdates{markersToAdd: $markersToAdd, ' - 'markerIdsToRemove: $markerIdsToRemove, ' - 'markersToChange: $markersToChange}'; - } + Set get markersToChange => objectsToChange; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart index 28c7ce9d33dd..f1cd7f4cb8eb 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart @@ -10,14 +10,14 @@ class PatternItem { const PatternItem._(this._json); /// A dot used in the stroke pattern for a [Polyline]. - static const PatternItem dot = PatternItem._(['dot']); + static const PatternItem dot = PatternItem._(['dot']); /// A dash used in the stroke pattern for a [Polyline]. /// /// [length] has to be non-negative. static PatternItem dash(double length) { assert(length >= 0.0); - return PatternItem._(['dash', length]); + return PatternItem._(['dash', length]); } /// A gap used in the stroke pattern for a [Polyline]. @@ -25,11 +25,11 @@ class PatternItem { /// [length] has to be non-negative. static PatternItem gap(double length) { assert(length >= 0.0); - return PatternItem._(['gap', length]); + return PatternItem._(['gap', length]); } - final dynamic _json; + final Object _json; /// Converts this object to something serializable in JSON. - dynamic toJson() => _json; + Object toJson() => _json; } 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..4e5e9bf13d84 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,9 +2,10 @@ // 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; +import 'package:meta/meta.dart' show immutable; import 'types.dart'; @@ -12,40 +13,22 @@ import 'types.dart'; /// /// This does not have to be globally unique, only unique among the list. @immutable -class PolygonId { +class PolygonId extends MapsObjectId { /// Creates an immutable identifier for a [Polygon]. - PolygonId(this.value) : assert(value != null); - - /// value of the [PolygonId]. - final String value; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final PolygonId typedOther = other; - return value == typedOther.value; - } - - @override - int get hashCode => value.hashCode; - - @override - String toString() { - return 'PolygonId{value: $value}'; - } + PolygonId(String value) : super(value); } /// Draws a polygon through geographical locations on the map. @immutable -class Polygon { +class Polygon implements MapsObject { /// Creates an immutable representation of a polygon through geographical locations on the map. const Polygon({ - @required this.polygonId, + required this.polygonId, this.consumeTapEvents = false, this.fillColor = Colors.black, this.geodesic = false, this.points = const [], + this.holes = const >[], this.strokeColor = Colors.black, this.strokeWidth = 10, this.visible = true, @@ -56,6 +39,9 @@ class Polygon { /// Uniquely identifies a [Polygon]. final PolygonId polygonId; + @override + PolygonId get mapsId => polygonId; + /// True if the [Polygon] consumes tap events. /// /// If this is false, [onTap] callback will not be triggered. @@ -77,6 +63,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; @@ -97,20 +91,21 @@ class Polygon { final int zIndex; /// Callbacks to receive tap events for polygon placed on this map. - final VoidCallback onTap; + final VoidCallback? onTap; /// Creates a new [Polygon] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Polygon copyWith({ - bool consumeTapEventsParam, - Color fillColorParam, - bool geodesicParam, - List pointsParam, - Color strokeColorParam, - int strokeWidthParam, - bool visibleParam, - int zIndexParam, - VoidCallback onTapParam, + bool? consumeTapEventsParam, + Color? fillColorParam, + bool? geodesicParam, + List? pointsParam, + List>? holesParam, + Color? strokeColorParam, + int? strokeWidthParam, + bool? visibleParam, + int? zIndexParam, + VoidCallback? onTapParam, }) { return Polygon( polygonId: polygonId, @@ -118,6 +113,7 @@ class Polygon { fillColor: fillColorParam ?? fillColor, geodesic: geodesicParam ?? geodesic, points: pointsParam ?? points, + holes: holesParam ?? holes, strokeColor: strokeColorParam ?? strokeColor, strokeWidth: strokeWidthParam ?? strokeWidth, visible: visibleParam ?? visible, @@ -132,10 +128,10 @@ class Polygon { } /// Converts this object to something serializable in JSON. - dynamic toJson() { - final Map json = {}; + Object toJson() { + final Map json = {}; - void addIfPresent(String fieldName, dynamic value) { + void addIfPresent(String fieldName, Object? value) { if (value != null) { json[fieldName] = value; } @@ -154,6 +150,10 @@ class Polygon { json['points'] = _pointsToJson(); } + if (holes != null) { + json['holes'] = _holesToJson(); + } + return json; } @@ -161,12 +161,13 @@ class Polygon { bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; - final Polygon typedOther = other; + final Polygon typedOther = other as Polygon; return polygonId == typedOther.polygonId && consumeTapEvents == typedOther.consumeTapEvents && fillColor == typedOther.fillColor && geodesic == typedOther.geodesic && listEquals(points, typedOther.points) && + DeepCollectionEquality().equals(holes, typedOther.holes) && visible == typedOther.visible && strokeColor == typedOther.strokeColor && strokeWidth == typedOther.strokeWidth && @@ -176,11 +177,23 @@ class Polygon { @override int get hashCode => polygonId.hashCode; - dynamic _pointsToJson() { - final List result = []; + Object _pointsToJson() { + final List result = []; for (final LatLng point in points) { result.add(point.toJson()); } 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/lib/src/types/polygon_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart index cc8b8e26c896..29b74aecbd66 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart @@ -2,109 +2,23 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - -import 'package:flutter/foundation.dart' show setEquals; - import 'types.dart'; -import 'utils/polygon.dart'; /// [Polygon] update events to be applied to the [GoogleMap]. /// /// Used in [GoogleMapController] when the map is updated. // (Do not re-export) -class PolygonUpdates { +class PolygonUpdates extends MapsObjectUpdates { /// Computes [PolygonUpdates] given previous and current [Polygon]s. - PolygonUpdates.from(Set previous, Set current) { - if (previous == null) { - previous = Set.identity(); - } - - if (current == null) { - current = Set.identity(); - } - - final Map previousPolygons = keyByPolygonId(previous); - final Map currentPolygons = keyByPolygonId(current); - - final Set prevPolygonIds = previousPolygons.keys.toSet(); - final Set currentPolygonIds = currentPolygons.keys.toSet(); - - Polygon idToCurrentPolygon(PolygonId id) { - return currentPolygons[id]; - } - - final Set _polygonIdsToRemove = - prevPolygonIds.difference(currentPolygonIds); - - final Set _polygonsToAdd = currentPolygonIds - .difference(prevPolygonIds) - .map(idToCurrentPolygon) - .toSet(); - - /// Returns `true` if [current] is not equals to previous one with the - /// same id. - bool hasChanged(Polygon current) { - final Polygon previous = previousPolygons[current.polygonId]; - return current != previous; - } - - final Set _polygonsToChange = currentPolygonIds - .intersection(prevPolygonIds) - .map(idToCurrentPolygon) - .where(hasChanged) - .toSet(); - - polygonsToAdd = _polygonsToAdd; - polygonIdsToRemove = _polygonIdsToRemove; - polygonsToChange = _polygonsToChange; - } + PolygonUpdates.from(Set previous, Set current) + : super.from(previous, current, objectName: 'polygon'); /// Set of Polygons to be added in this update. - Set polygonsToAdd; + Set get polygonsToAdd => objectsToAdd; /// Set of PolygonIds to be removed in this update. - Set polygonIdsToRemove; + Set get polygonIdsToRemove => objectIdsToRemove.cast(); /// Set of Polygons to be changed in this update. - Set polygonsToChange; - - /// Converts this object to something serializable in JSON. - Map toJson() { - final Map updateMap = {}; - - void addIfNonNull(String fieldName, dynamic value) { - if (value != null) { - updateMap[fieldName] = value; - } - } - - addIfNonNull('polygonsToAdd', serializePolygonSet(polygonsToAdd)); - addIfNonNull('polygonsToChange', serializePolygonSet(polygonsToChange)); - addIfNonNull('polygonIdsToRemove', - polygonIdsToRemove.map((PolygonId m) => m.value).toList()); - - return updateMap; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final PolygonUpdates typedOther = other; - return setEquals(polygonsToAdd, typedOther.polygonsToAdd) && - setEquals(polygonIdsToRemove, typedOther.polygonIdsToRemove) && - setEquals(polygonsToChange, typedOther.polygonsToChange); - } - - @override - int get hashCode => - hashValues(polygonsToAdd, polygonIdsToRemove, polygonsToChange); - - @override - String toString() { - return '_PolygonUpdates{polygonsToAdd: $polygonsToAdd, ' - 'polygonIdsToRemove: $polygonIdsToRemove, ' - 'polygonsToChange: $polygonsToChange}'; - } + Set get polygonsToChange => objectsToChange; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart index ae5c3b976352..3f87395164f6 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart @@ -4,7 +4,7 @@ import 'package:flutter/foundation.dart' show listEquals, VoidCallback; import 'package:flutter/material.dart' show Color, Colors; -import 'package:meta/meta.dart' show immutable, required; +import 'package:meta/meta.dart' show immutable; import 'types.dart'; @@ -12,38 +12,19 @@ import 'types.dart'; /// /// This does not have to be globally unique, only unique among the list. @immutable -class PolylineId { +class PolylineId extends MapsObjectId { /// Creates an immutable object representing a [PolylineId] among [GoogleMap] polylines. /// /// An [AssertionError] will be thrown if [value] is null. - PolylineId(this.value) : assert(value != null); - - /// value of the [PolylineId]. - final String value; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final PolylineId typedOther = other; - return value == typedOther.value; - } - - @override - int get hashCode => value.hashCode; - - @override - String toString() { - return 'PolylineId{value: $value}'; - } + PolylineId(String value) : super(value); } /// Draws a line through geographical locations on the map. @immutable -class Polyline { +class Polyline implements MapsObject { /// Creates an immutable object representing a line drawn through geographical locations on the map. const Polyline({ - @required this.polylineId, + required this.polylineId, this.consumeTapEvents = false, this.color = Colors.black, this.endCap = Cap.buttCap, @@ -61,6 +42,9 @@ class Polyline { /// Uniquely identifies a [Polyline]. final PolylineId polylineId; + @override + PolylineId get mapsId => polylineId; + /// True if the [Polyline] consumes tap events. /// /// If this is false, [onTap] callback will not be triggered. @@ -129,23 +113,23 @@ class Polyline { final int zIndex; /// Callbacks to receive tap events for polyline placed on this map. - final VoidCallback onTap; + final VoidCallback? onTap; /// Creates a new [Polyline] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Polyline copyWith({ - Color colorParam, - bool consumeTapEventsParam, - Cap endCapParam, - bool geodesicParam, - JointType jointTypeParam, - List patternsParam, - List pointsParam, - Cap startCapParam, - bool visibleParam, - int widthParam, - int zIndexParam, - VoidCallback onTapParam, + Color? colorParam, + bool? consumeTapEventsParam, + Cap? endCapParam, + bool? geodesicParam, + JointType? jointTypeParam, + List? patternsParam, + List? pointsParam, + Cap? startCapParam, + bool? visibleParam, + int? widthParam, + int? zIndexParam, + VoidCallback? onTapParam, }) { return Polyline( polylineId: polylineId, @@ -174,10 +158,10 @@ class Polyline { } /// Converts this object to something serializable in JSON. - dynamic toJson() { - final Map json = {}; + Object toJson() { + final Map json = {}; - void addIfPresent(String fieldName, dynamic value) { + void addIfPresent(String fieldName, Object? value) { if (value != null) { json[fieldName] = value; } @@ -186,10 +170,10 @@ class Polyline { addIfPresent('polylineId', polylineId.value); addIfPresent('consumeTapEvents', consumeTapEvents); addIfPresent('color', color.value); - addIfPresent('endCap', endCap?.toJson()); + addIfPresent('endCap', endCap.toJson()); addIfPresent('geodesic', geodesic); - addIfPresent('jointType', jointType?.value); - addIfPresent('startCap', startCap?.toJson()); + addIfPresent('jointType', jointType.value); + addIfPresent('startCap', startCap.toJson()); addIfPresent('visible', visible); addIfPresent('width', width); addIfPresent('zIndex', zIndex); @@ -209,7 +193,7 @@ class Polyline { bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; - final Polyline typedOther = other; + final Polyline typedOther = other as Polyline; return polylineId == typedOther.polylineId && consumeTapEvents == typedOther.consumeTapEvents && color == typedOther.color && @@ -227,16 +211,16 @@ class Polyline { @override int get hashCode => polylineId.hashCode; - dynamic _pointsToJson() { - final List result = []; + Object _pointsToJson() { + final List result = []; for (final LatLng point in points) { result.add(point.toJson()); } return result; } - dynamic _patternToJson() { - final List result = []; + Object _patternToJson() { + final List result = []; for (final PatternItem patternItem in patterns) { if (patternItem != null) { result.add(patternItem.toJson()); diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart index f871928c0ac4..60e0bfe6c7ce 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart @@ -2,110 +2,24 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - -import 'package:flutter/foundation.dart' show setEquals; - -import 'utils/polyline.dart'; import 'types.dart'; /// [Polyline] update events to be applied to the [GoogleMap]. /// /// Used in [GoogleMapController] when the map is updated. // (Do not re-export) -class PolylineUpdates { +class PolylineUpdates extends MapsObjectUpdates { /// Computes [PolylineUpdates] given previous and current [Polyline]s. - PolylineUpdates.from(Set previous, Set current) { - if (previous == null) { - previous = Set.identity(); - } - - if (current == null) { - current = Set.identity(); - } - - final Map previousPolylines = - keyByPolylineId(previous); - final Map currentPolylines = keyByPolylineId(current); - - final Set prevPolylineIds = previousPolylines.keys.toSet(); - final Set currentPolylineIds = currentPolylines.keys.toSet(); - - Polyline idToCurrentPolyline(PolylineId id) { - return currentPolylines[id]; - } - - final Set _polylineIdsToRemove = - prevPolylineIds.difference(currentPolylineIds); - - final Set _polylinesToAdd = currentPolylineIds - .difference(prevPolylineIds) - .map(idToCurrentPolyline) - .toSet(); - - /// Returns `true` if [current] is not equals to previous one with the - /// same id. - bool hasChanged(Polyline current) { - final Polyline previous = previousPolylines[current.polylineId]; - return current != previous; - } - - final Set _polylinesToChange = currentPolylineIds - .intersection(prevPolylineIds) - .map(idToCurrentPolyline) - .where(hasChanged) - .toSet(); - - polylinesToAdd = _polylinesToAdd; - polylineIdsToRemove = _polylineIdsToRemove; - polylinesToChange = _polylinesToChange; - } + PolylineUpdates.from(Set previous, Set current) + : super.from(previous, current, objectName: 'polyline'); /// Set of Polylines to be added in this update. - Set polylinesToAdd; + Set get polylinesToAdd => objectsToAdd; /// Set of PolylineIds to be removed in this update. - Set polylineIdsToRemove; + Set get polylineIdsToRemove => + objectIdsToRemove.cast(); /// Set of Polylines to be changed in this update. - Set polylinesToChange; - - /// Converts this object to something serializable in JSON. - Map toJson() { - final Map updateMap = {}; - - void addIfNonNull(String fieldName, dynamic value) { - if (value != null) { - updateMap[fieldName] = value; - } - } - - addIfNonNull('polylinesToAdd', serializePolylineSet(polylinesToAdd)); - addIfNonNull('polylinesToChange', serializePolylineSet(polylinesToChange)); - addIfNonNull('polylineIdsToRemove', - polylineIdsToRemove.map((PolylineId m) => m.value).toList()); - - return updateMap; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final PolylineUpdates typedOther = other; - return setEquals(polylinesToAdd, typedOther.polylinesToAdd) && - setEquals(polylineIdsToRemove, typedOther.polylineIdsToRemove) && - setEquals(polylinesToChange, typedOther.polylinesToChange); - } - - @override - int get hashCode => - hashValues(polylinesToAdd, polylineIdsToRemove, polylinesToChange); - - @override - String toString() { - return '_PolylineUpdates{polylinesToAdd: $polylinesToAdd, ' - 'polylineIdsToRemove: $polylineIdsToRemove, ' - 'polylinesToChange: $polylinesToChange}'; - } + Set get polylinesToChange => objectsToChange; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart index 965db7969bc2..af7a951a149c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart @@ -4,7 +4,7 @@ import 'dart:ui' show hashValues; -import 'package:meta/meta.dart' show immutable, required; +import 'package:meta/meta.dart' show immutable; /// Represents a point coordinate in the [GoogleMap]'s view. /// @@ -15,8 +15,8 @@ import 'package:meta/meta.dart' show immutable, required; class ScreenCoordinate { /// Creates an immutable representation of a point coordinate in the [GoogleMap]'s view. const ScreenCoordinate({ - @required this.x, - @required this.y, + required this.x, + required this.y, }); /// Represents the number of pixels from the left of the [GoogleMap]. @@ -26,7 +26,7 @@ class ScreenCoordinate { final int y; /// Converts this object to something serializable in JSON. - dynamic toJson() { + Object toJson() { return { "x": x, "y": y, diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart new file mode 100644 index 000000000000..bfc322856b5a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart @@ -0,0 +1,42 @@ +// 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:typed_data'; +import 'package:meta/meta.dart' show immutable; + +/// Contains information about a Tile that is returned by a [TileProvider]. +@immutable +class Tile { + /// Creates an immutable representation of a [Tile] to draw by [TileProvider]. + const Tile(this.width, this.height, this.data); + + /// The width of the image encoded by data in logical pixels. + final int width; + + /// The height of the image encoded by data in logical pixels. + final int height; + + /// A byte array containing the image data. + /// + /// The image data format must be natively supported for decoding by the platform. + /// e.g on Android it can only be one of the [supported image formats for decoding](https://developer.android.com/guide/topics/media/media-formats#image-formats). + final Uint8List? data; + + /// Converts this object to JSON. + Object toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, Object? value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('width', width); + addIfPresent('height', height); + addIfPresent('data', data); + + return json; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart new file mode 100644 index 000000000000..e31bfb461fb4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart @@ -0,0 +1,148 @@ +// 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:ui' show hashValues; +import 'package:flutter/foundation.dart'; + +import 'types.dart'; +import 'package:meta/meta.dart' show immutable; + +/// Uniquely identifies a [TileOverlay] among [GoogleMap] tile overlays. +@immutable +class TileOverlayId extends MapsObjectId { + /// Creates an immutable identifier for a [TileOverlay]. + TileOverlayId(String value) : super(value); +} + +/// A set of images which are displayed on top of the base map tiles. +/// +/// These tiles may be transparent, allowing you to add features to existing maps. +/// +/// ## Tile Coordinates +/// +/// Note that the world is projected using the Mercator projection +/// (see [Wikipedia](https://en.wikipedia.org/wiki/Mercator_projection)) with the left (west) side +/// of the map corresponding to -180 degrees of longitude and the right (east) side of the map +/// corresponding to 180 degrees of longitude. To make the map square, the top (north) side of the +/// map corresponds to 85.0511 degrees of latitude and the bottom (south) side of the map +/// corresponds to -85.0511 degrees of latitude. Areas outside this latitude range are not rendered. +/// +/// At each zoom level, the map is divided into tiles and only the tiles that overlap the screen are +/// downloaded and rendered. Each tile is square and the map is divided into tiles as follows: +/// +/// * At zoom level 0, one tile represents the entire world. The coordinates of that tile are +/// (x, y) = (0, 0). +/// * At zoom level 1, the world is divided into 4 tiles arranged in a 2 x 2 grid. +/// * ... +/// * At zoom level N, the world is divided into 4N tiles arranged in a 2N x 2N grid. +/// +/// Note that the minimum zoom level that the camera supports (which can depend on various factors) +/// is GoogleMap.getMinZoomLevel and the maximum zoom level is GoogleMap.getMaxZoomLevel. +/// +/// The coordinates of the tiles are measured from the top left (northwest) corner of the map. +/// At zoom level N, the x values of the tile coordinates range from 0 to 2N - 1 and increase from +/// west to east and the y values range from 0 to 2N - 1 and increase from north to south. +/// +class TileOverlay implements MapsObject { + /// Creates an immutable representation of a [TileOverlay] to draw on [GoogleMap]. + const TileOverlay({ + required this.tileOverlayId, + this.fadeIn = true, + this.tileProvider, + this.transparency = 0.0, + this.zIndex = 0, + this.visible = true, + this.tileSize = 256, + }) : assert(transparency >= 0.0 && transparency <= 1.0); + + /// Uniquely identifies a [TileOverlay]. + final TileOverlayId tileOverlayId; + + @override + TileOverlayId get mapsId => tileOverlayId; + + /// Whether the tiles should fade in. The default is true. + final bool fadeIn; + + /// The tile provider to use for this tile overlay. + final TileProvider? tileProvider; + + /// The transparency of the tile overlay. The default transparency is 0 (opaque). + final double transparency; + + /// The tile overlay's zIndex, i.e., the order in which it will be drawn where + /// overlays with larger values are drawn above those with lower values + final int zIndex; + + /// The visibility for the tile overlay. The default visibility is true. + final bool visible; + + /// Specifies the number of logical pixels (not points) that the returned tile images will prefer + /// to display as. iOS only. + /// + /// Defaults to 256, which is the traditional size of Google Maps tiles. + /// As an example, an application developer may wish to provide retina tiles (512 pixel edge length) + /// on retina devices, to keep the same number of tiles per view as the default value of 256 + /// would give on a non-retina device. + final int tileSize; + + /// Creates a new [TileOverlay] object whose values are the same as this instance, + /// unless overwritten by the specified parameters. + TileOverlay copyWith({ + bool? fadeInParam, + double? transparencyParam, + int? zIndexParam, + bool? visibleParam, + int? tileSizeParam, + }) { + return TileOverlay( + tileOverlayId: tileOverlayId, + fadeIn: fadeInParam ?? fadeIn, + transparency: transparencyParam ?? transparency, + zIndex: zIndexParam ?? zIndex, + visible: visibleParam ?? visible, + tileSize: tileSizeParam ?? tileSize, + ); + } + + TileOverlay clone() => copyWith(); + + /// Converts this object to JSON. + Object toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, Object? value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('tileOverlayId', tileOverlayId.value); + addIfPresent('fadeIn', fadeIn); + addIfPresent('transparency', transparency); + addIfPresent('zIndex', zIndex); + addIfPresent('visible', visible); + addIfPresent('tileSize', tileSize); + + return json; + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is TileOverlay && + tileOverlayId == other.tileOverlayId && + fadeIn == other.fadeIn && + transparency == other.transparency && + zIndex == other.zIndex && + visible == other.visible && + tileSize == other.tileSize; + } + + @override + int get hashCode => hashValues( + tileOverlayId, fadeIn, transparency, zIndex, visible, tileSize); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.dart new file mode 100644 index 000000000000..1436880e9626 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.dart @@ -0,0 +1,22 @@ +// 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 'types.dart'; + +/// Update specification for a set of [TileOverlay]s. +class TileOverlayUpdates extends MapsObjectUpdates { + /// Computes [TileOverlayUpdates] given previous and current [TileOverlay]s. + TileOverlayUpdates.from(Set previous, Set current) + : super.from(previous, current, objectName: 'tileOverlay'); + + /// Set of TileOverlays to be added in this update. + Set get tileOverlaysToAdd => objectsToAdd; + + /// Set of TileOverlayIds to be removed in this update. + Set get tileOverlayIdsToRemove => + objectIdsToRemove.cast(); + + /// Set of TileOverlays to be changed in this update. + Set get tileOverlaysToChange => objectsToChange; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.dart new file mode 100644 index 000000000000..abaf38ba719d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.dart @@ -0,0 +1,16 @@ +// 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 'types.dart'; + +/// An interface for a class that provides the tile images for a TileOverlay. +abstract class TileProvider { + /// Stub tile that is used to indicate that no tile exists for a specific tile coordinate. + static const Tile noTile = Tile(-1, -1, null); + + /// Returns the tile to be used for this tile coordinate. + /// + /// See [TileOverlay] for the specification of tile coordinates. + Future getTile(int x, int y, int? zoom); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart index e56c3a5dd646..9b7da87f2cb0 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart @@ -11,6 +11,8 @@ export 'circle_updates.dart'; export 'circle.dart'; export 'joint_type.dart'; export 'location.dart'; +export 'maps_object_updates.dart'; +export 'maps_object.dart'; export 'marker_updates.dart'; export 'marker.dart'; export 'pattern_item.dart'; @@ -19,6 +21,9 @@ export 'polygon.dart'; export 'polyline_updates.dart'; export 'polyline.dart'; export 'screen_coordinate.dart'; +export 'tile.dart'; +export 'tile_overlay.dart'; +export 'tile_provider.dart'; export 'ui.dart'; // Export the utils, they're used by the Widget @@ -26,3 +31,4 @@ export 'utils/circle.dart'; export 'utils/marker.dart'; export 'utils/polygon.dart'; export 'utils/polyline.dart'; +export 'utils/tile_overlay.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart index 8d84171bac03..1c030e338353 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart @@ -39,19 +39,19 @@ class CameraTargetBounds { /// The geographical bounding box for the map camera target. /// /// A null value means the camera target is unbounded. - final LatLngBounds bounds; + final LatLngBounds? bounds; /// Unbounded camera target. static const CameraTargetBounds unbounded = CameraTargetBounds(null); /// Converts this object to something serializable in JSON. - dynamic toJson() => [bounds?.toJson()]; + Object toJson() => [bounds?.toJson()]; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (identical(this, other)) return true; if (runtimeType != other.runtimeType) return false; - final CameraTargetBounds typedOther = other; + final CameraTargetBounds typedOther = other as CameraTargetBounds; return bounds == typedOther.bounds; } @@ -76,23 +76,23 @@ class MinMaxZoomPreference { : assert(minZoom == null || maxZoom == null || minZoom <= maxZoom); /// The preferred minimum zoom level or null, if unbounded from below. - final double minZoom; + final double? minZoom; /// The preferred maximum zoom level or null, if unbounded from above. - final double maxZoom; + final double? maxZoom; /// Unbounded zooming. static const MinMaxZoomPreference unbounded = MinMaxZoomPreference(null, null); /// Converts this object to something serializable in JSON. - dynamic toJson() => [minZoom, maxZoom]; + Object toJson() => [minZoom, maxZoom]; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (identical(this, other)) return true; if (runtimeType != other.runtimeType) return false; - final MinMaxZoomPreference typedOther = other; + final MinMaxZoomPreference typedOther = other as MinMaxZoomPreference; return minZoom == typedOther.minZoom && maxZoom == typedOther.maxZoom; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart index 5c3af96f8e02..18bd31ec7df6 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart @@ -3,20 +3,14 @@ // found in the LICENSE file. import '../types.dart'; +import 'maps_object.dart'; /// Converts an [Iterable] of Circles in a Map of CircleId -> Circle. Map keyByCircleId(Iterable circles) { - if (circles == null) { - return {}; - } - return Map.fromEntries(circles.map((Circle circle) => - MapEntry(circle.circleId, circle.clone()))); + return keyByMapsObjectId(circles).cast(); } /// Converts a Set of Circles into something serializable in JSON. -List> serializeCircleSet(Set circles) { - if (circles == null) { - return null; - } - return circles.map>((Circle p) => p.toJson()).toList(); +Object serializeCircleSet(Set circles) { + return serializeMapsObjectSet(circles); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.dart new file mode 100644 index 000000000000..fa5a7e7591ee --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.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. + +import '../maps_object.dart'; + +/// Converts an [Iterable] of [MapsObject]s in a Map of [MapObjectId] -> [MapObject]. +Map, T> keyByMapsObjectId( + Iterable objects) { + return Map, T>.fromEntries(objects.map((T object) => + MapEntry, T>( + object.mapsId as MapsObjectId, object.clone()))); +} + +/// Converts a Set of [MapsObject]s into something serializable in JSON. +Object serializeMapsObjectSet(Set mapsObjects) { + return mapsObjects.map((MapsObject p) => p.toJson()).toList(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart index 7a2c76d8055b..057bebdc8b8a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart @@ -3,20 +3,14 @@ // found in the LICENSE file. import '../types.dart'; +import 'maps_object.dart'; /// Converts an [Iterable] of Markers in a Map of MarkerId -> Marker. Map keyByMarkerId(Iterable markers) { - if (markers == null) { - return {}; - } - return Map.fromEntries(markers.map((Marker marker) => - MapEntry(marker.markerId, marker.clone()))); + return keyByMapsObjectId(markers).cast(); } /// Converts a Set of Markers into something serializable in JSON. -List> serializeMarkerSet(Set markers) { - if (markers == null) { - return null; - } - return markers.map>((Marker m) => m.toJson()).toList(); +Object serializeMarkerSet(Set markers) { + return serializeMapsObjectSet(markers); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart index 9434ddaa077d..050ecafce31f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart @@ -3,20 +3,14 @@ // found in the LICENSE file. import '../types.dart'; +import 'maps_object.dart'; /// Converts an [Iterable] of Polygons in a Map of PolygonId -> Polygon. Map keyByPolygonId(Iterable polygons) { - if (polygons == null) { - return {}; - } - return Map.fromEntries(polygons.map((Polygon polygon) => - MapEntry(polygon.polygonId, polygon.clone()))); + return keyByMapsObjectId(polygons).cast(); } /// Converts a Set of Polygons into something serializable in JSON. -List> serializePolygonSet(Set polygons) { - if (polygons == null) { - return null; - } - return polygons.map>((Polygon p) => p.toJson()).toList(); +Object serializePolygonSet(Set polygons) { + return serializeMapsObjectSet(polygons); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart index 9cef6319ddb5..8f4098feedf7 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart @@ -3,23 +3,14 @@ // found in the LICENSE file. import '../types.dart'; +import 'maps_object.dart'; /// Converts an [Iterable] of Polylines in a Map of PolylineId -> Polyline. Map keyByPolylineId(Iterable polylines) { - if (polylines == null) { - return {}; - } - return Map.fromEntries(polylines.map( - (Polyline polyline) => MapEntry( - polyline.polylineId, polyline.clone()))); + return keyByMapsObjectId(polylines).cast(); } /// Converts a Set of Polylines into something serializable in JSON. -List> serializePolylineSet(Set polylines) { - if (polylines == null) { - return null; - } - return polylines - .map>((Polyline p) => p.toJson()) - .toList(); +Object serializePolylineSet(Set polylines) { + return serializeMapsObjectSet(polylines); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.dart new file mode 100644 index 000000000000..336f814d329a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.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. + +import '../types.dart'; +import 'maps_object.dart'; + +/// Converts an [Iterable] of TileOverlay in a Map of TileOverlayId -> TileOverlay. +Map keyTileOverlayId( + Iterable tileOverlays) { + return keyByMapsObjectId(tileOverlays) + .cast(); +} + +/// Converts a Set of TileOverlays into something serializable in JSON. +Object serializeTileOverlaySet(Set tileOverlays) { + return serializeMapsObjectSet(tileOverlays); +} 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 fd3a1c434960..602efe3e6c62 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,21 +3,22 @@ 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.4 +version: 2.0.0 dependencies: flutter: sdk: flutter - meta: ^1.0.5 - plugin_platform_interface: ^1.0.1 - stream_transform: ^1.2.0 + meta: ^1.3.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" + stream_transform: ^2.0.0 + collection: ^1.15.0 dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 + mockito: ^5.0.0-nullsafety.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + sdk: '>=2.12.0-259.9.beta <3.0.0' + flutter: ">=1.9.1+hotfix.4" diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart index a003b94d544c..582acea54dd1 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart @@ -45,14 +45,11 @@ void main() { log.add(methodCall); }); -// final MethodChannelGoogleMapsFlutter map = MethodChannelGoogleMapsFlutter(0); - tearDown(() { log.clear(); }); test('foo', () async { -// await map.foo(); expect( log, [], diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart new file mode 100644 index 000000000000..afcf57debef1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart @@ -0,0 +1,167 @@ +// 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:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$BitmapDescriptor', () { + test('toJson / fromJson', () { + final descriptor = + BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); + final json = descriptor.toJson(); + + // Rehydrate a new bitmap descriptor... + // ignore: deprecated_member_use_from_same_package + final descriptorFromJson = BitmapDescriptor.fromJson(json); + + expect(descriptorFromJson, isNot(descriptor)); // New instance + expect(identical(descriptorFromJson.toJson(), json), isTrue); // Same JSON + }); + + group('fromJson validation', () { + group('type validation', () { + test('correct type', () { + expect(BitmapDescriptor.fromJson(['defaultMarker']), + isA()); + }); + test('wrong type', () { + expect(() { + BitmapDescriptor.fromJson(['bogusType']); + }, throwsAssertionError); + }); + }); + group('defaultMarker', () { + test('hue is null', () { + expect(BitmapDescriptor.fromJson(['defaultMarker']), + isA()); + }); + test('hue is number', () { + expect(BitmapDescriptor.fromJson(['defaultMarker', 158]), + isA()); + }); + test('hue is not number', () { + expect(() { + BitmapDescriptor.fromJson(['defaultMarker', 'nope']); + }, throwsAssertionError); + }); + test('hue is out of range', () { + expect(() { + BitmapDescriptor.fromJson(['defaultMarker', -1]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson(['defaultMarker', 361]); + }, throwsAssertionError); + }); + }); + group('fromBytes', () { + test('with bytes', () { + expect( + BitmapDescriptor.fromJson([ + 'fromBytes', + Uint8List.fromList([1, 2, 3]) + ]), + isA()); + }); + test('without bytes', () { + expect(() { + BitmapDescriptor.fromJson(['fromBytes', null]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson(['fromBytes', []]); + }, throwsAssertionError); + }); + }); + group('fromAsset', () { + test('name is passed', () { + expect(BitmapDescriptor.fromJson(['fromAsset', 'some/path.png']), + isA()); + }); + test('name cannot be null or empty', () { + expect(() { + BitmapDescriptor.fromJson(['fromAsset', null]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson(['fromAsset', '']); + }, throwsAssertionError); + }); + test('package is passed', () { + expect( + BitmapDescriptor.fromJson( + ['fromAsset', 'some/path.png', 'some_package']), + isA()); + }); + test('package cannot be null or empty', () { + expect(() { + BitmapDescriptor.fromJson(['fromAsset', 'some/path.png', null]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson(['fromAsset', 'some/path.png', '']); + }, throwsAssertionError); + }); + }); + group('fromAssetImage', () { + test('name and dpi passed', () { + expect( + BitmapDescriptor.fromJson( + ['fromAssetImage', 'some/path.png', 1.0]), + isA()); + }); + test('name cannot be null or empty', () { + expect(() { + BitmapDescriptor.fromJson(['fromAssetImage', null, 1.0]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson(['fromAssetImage', '', 1.0]); + }, throwsAssertionError); + }); + test('dpi must be number', () { + expect(() { + BitmapDescriptor.fromJson( + ['fromAssetImage', 'some/path.png', null]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson( + ['fromAssetImage', 'some/path.png', 'one']); + }, throwsAssertionError); + }); + test('with optional [width, height] List', () { + expect( + BitmapDescriptor.fromJson([ + 'fromAssetImage', + 'some/path.png', + 1.0, + [640, 480] + ]), + isA()); + }); + test( + 'optional [width, height] List cannot be null or not contain 2 elements', + () { + expect(() { + BitmapDescriptor.fromJson( + ['fromAssetImage', 'some/path.png', 1.0, null]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson( + ['fromAssetImage', 'some/path.png', 1.0, []]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson([ + 'fromAssetImage', + 'some/path.png', + 1.0, + [640, 480, 1024] + ]); + }, throwsAssertionError); + }); + }); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/camera_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/camera_test.dart new file mode 100644 index 000000000000..3b6d237e05d4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/camera_test.dart @@ -0,0 +1,23 @@ +// 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 'package:flutter_test/flutter_test.dart'; + +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('toMap / fromMap', () { + final cameraPosition = CameraPosition( + target: LatLng(10.0, 15.0), bearing: 0.5, tilt: 30.0, zoom: 1.5); + // Cast to to ensure that recreating from JSON, where + // type information will have likely been lost, still works. + final json = (cameraPosition.toMap() as Map) + .cast(); + final cameraPositionFromJson = CameraPosition.fromMap(json); + + expect(cameraPosition, cameraPositionFromJson); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.dart new file mode 100644 index 000000000000..65692bd2a385 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.dart @@ -0,0 +1,45 @@ +// 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 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/utils/maps_object.dart'; + +import 'test_maps_object.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('keyByMapsObjectId', () async { + final MapsObjectId id1 = MapsObjectId('1'); + final MapsObjectId id2 = MapsObjectId('2'); + final MapsObjectId id3 = MapsObjectId('3'); + final TestMapsObject object1 = TestMapsObject(id1); + final TestMapsObject object2 = TestMapsObject(id2, data: 2); + final TestMapsObject object3 = TestMapsObject(id3); + expect( + keyByMapsObjectId({object1, object2, object3}), + , TestMapsObject>{ + id1: object1, + id2: object2, + id3: object3, + }); + }); + + test('serializeMapsObjectSet', () async { + final MapsObjectId id1 = MapsObjectId('1'); + final MapsObjectId id2 = MapsObjectId('2'); + final MapsObjectId id3 = MapsObjectId('3'); + final TestMapsObject object1 = TestMapsObject(id1); + final TestMapsObject object2 = TestMapsObject(id2, data: 2); + final TestMapsObject object3 = TestMapsObject(id3); + expect( + serializeMapsObjectSet({object1, object2, object3}), + >[ + {'id': '1'}, + {'id': '2'}, + {'id': '3'} + ]); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart new file mode 100644 index 000000000000..68f4c587c2f2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart @@ -0,0 +1,160 @@ +// 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:ui' show hashValues, hashList; + +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/utils/maps_object.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/maps_object_updates.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/maps_object.dart'; + +import 'test_maps_object.dart'; + +class TestMapsObjectUpdate extends MapsObjectUpdates { + TestMapsObjectUpdate.from( + Set previous, Set current) + : super.from(previous, current, objectName: 'testObject'); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('tile overlay updates tests', () { + test('Correctly set toRemove, toAdd and toChange', () async { + final TestMapsObject to1 = + TestMapsObject(MapsObjectId('id1')); + final TestMapsObject to2 = + TestMapsObject(MapsObjectId('id2')); + final TestMapsObject to3 = + TestMapsObject(MapsObjectId('id3')); + final TestMapsObject to3Changed = + TestMapsObject(MapsObjectId('id3'), data: 2); + final TestMapsObject to4 = + TestMapsObject(MapsObjectId('id4')); + final Set previous = + Set.from([to1, to2, to3]); + final Set current = + Set.from([to2, to3Changed, to4]); + final TestMapsObjectUpdate updates = + TestMapsObjectUpdate.from(previous, current); + + final Set> toRemove = Set.from( + >[MapsObjectId('id1')]); + expect(updates.objectIdsToRemove, toRemove); + + final Set toAdd = Set.from([to4]); + expect(updates.objectsToAdd, toAdd); + + final Set toChange = + Set.from([to3Changed]); + expect(updates.objectsToChange, toChange); + }); + + test('toJson', () async { + final TestMapsObject to1 = + TestMapsObject(MapsObjectId('id1')); + final TestMapsObject to2 = + TestMapsObject(MapsObjectId('id2')); + final TestMapsObject to3 = + TestMapsObject(MapsObjectId('id3')); + final TestMapsObject to3Changed = + TestMapsObject(MapsObjectId('id3'), data: 2); + final TestMapsObject to4 = + TestMapsObject(MapsObjectId('id4')); + final Set previous = + Set.from([to1, to2, to3]); + final Set current = + Set.from([to2, to3Changed, to4]); + final TestMapsObjectUpdate updates = + TestMapsObjectUpdate.from(previous, current); + + final Object json = updates.toJson(); + expect(json, { + 'testObjectsToAdd': serializeMapsObjectSet(updates.objectsToAdd), + 'testObjectsToChange': serializeMapsObjectSet(updates.objectsToChange), + 'testObjectIdsToRemove': updates.objectIdsToRemove + .map((MapsObjectId m) => m.value) + .toList() + }); + }); + + test('equality', () async { + final TestMapsObject to1 = + TestMapsObject(MapsObjectId('id1')); + final TestMapsObject to2 = + TestMapsObject(MapsObjectId('id2')); + final TestMapsObject to3 = + TestMapsObject(MapsObjectId('id3')); + final TestMapsObject to3Changed = + TestMapsObject(MapsObjectId('id3'), data: 2); + final TestMapsObject to4 = + TestMapsObject(MapsObjectId('id4')); + final Set previous = + Set.from([to1, to2, to3]); + final Set current1 = + Set.from([to2, to3Changed, to4]); + final Set current2 = + Set.from([to2, to3Changed, to4]); + final Set current3 = Set.from([to2, to4]); + final TestMapsObjectUpdate updates1 = + TestMapsObjectUpdate.from(previous, current1); + final TestMapsObjectUpdate updates2 = + TestMapsObjectUpdate.from(previous, current2); + final TestMapsObjectUpdate updates3 = + TestMapsObjectUpdate.from(previous, current3); + expect(updates1, updates2); + expect(updates1, isNot(updates3)); + }); + + test('hashCode', () async { + final TestMapsObject to1 = + TestMapsObject(MapsObjectId('id1')); + final TestMapsObject to2 = + TestMapsObject(MapsObjectId('id2')); + final TestMapsObject to3 = + TestMapsObject(MapsObjectId('id3')); + final TestMapsObject to3Changed = + TestMapsObject(MapsObjectId('id3'), data: 2); + final TestMapsObject to4 = + TestMapsObject(MapsObjectId('id4')); + final Set previous = + Set.from([to1, to2, to3]); + final Set current = + Set.from([to2, to3Changed, to4]); + final TestMapsObjectUpdate updates = + TestMapsObjectUpdate.from(previous, current); + expect( + updates.hashCode, + hashValues( + hashList(updates.objectsToAdd), + hashList(updates.objectIdsToRemove), + hashList(updates.objectsToChange))); + }); + + test('toString', () async { + final TestMapsObject to1 = + TestMapsObject(MapsObjectId('id1')); + final TestMapsObject to2 = + TestMapsObject(MapsObjectId('id2')); + final TestMapsObject to3 = + TestMapsObject(MapsObjectId('id3')); + final TestMapsObject to3Changed = + TestMapsObject(MapsObjectId('id3'), data: 2); + final TestMapsObject to4 = + TestMapsObject(MapsObjectId('id4')); + final Set previous = + Set.from([to1, to2, to3]); + final Set current = + Set.from([to2, to3Changed, to4]); + final TestMapsObjectUpdate updates = + TestMapsObjectUpdate.from(previous, current); + expect( + updates.toString(), + 'TestMapsObjectUpdate(add: ${updates.objectsToAdd}, ' + 'remove: ${updates.objectIdsToRemove}, ' + 'change: ${updates.objectsToChange})'); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart new file mode 100644 index 000000000000..e15c73f08a54 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart @@ -0,0 +1,46 @@ +// 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:ui' show hashValues; +import 'package:flutter/rendering.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/maps_object_updates.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/maps_object.dart'; + +/// A trivial TestMapsObject implementation for testing updates with. +class TestMapsObject implements MapsObject { + TestMapsObject(this.mapsId, {this.data = 1}); + + final MapsObjectId mapsId; + + final int data; + + @override + TestMapsObject clone() { + return TestMapsObject(mapsId, data: data); + } + + @override + Object toJson() { + return {'id': mapsId.value}; + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is TestMapsObject && + mapsId == other.mapsId && + data == other.data; + } + + @override + int get hashCode => hashValues(mapsId, data); +} + +class TestMapsObjectUpdate extends MapsObjectUpdates { + TestMapsObjectUpdate.from( + Set previous, Set current) + : super.from(previous, current, objectName: 'testObject'); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart new file mode 100644 index 000000000000..87380fdd651b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart @@ -0,0 +1,109 @@ +// 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:ui' show hashValues; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('tile overlay id tests', () { + test('equality', () async { + final TileOverlayId id1 = TileOverlayId('1'); + final TileOverlayId id2 = TileOverlayId('1'); + final TileOverlayId id3 = TileOverlayId('2'); + expect(id1, id2); + expect(id1, isNot(id3)); + }); + + test('toString', () async { + final TileOverlayId id1 = TileOverlayId('1'); + expect(id1.toString(), 'TileOverlayId(1)'); + }); + }); + + group('tile overlay tests', () { + test('toJson returns correct format', () async { + final TileOverlay tileOverlay = TileOverlay( + tileOverlayId: TileOverlayId('id'), + fadeIn: false, + tileProvider: null, + transparency: 0.1, + zIndex: 1, + visible: false, + tileSize: 128); + final Object json = tileOverlay.toJson(); + expect(json, { + 'tileOverlayId': 'id', + 'fadeIn': false, + 'transparency': moreOrLessEquals(0.1), + 'zIndex': 1, + 'visible': false, + 'tileSize': 128, + }); + }); + + test('invalid transparency throws', () async { + expect( + () => TileOverlay( + tileOverlayId: TileOverlayId('id1'), transparency: -0.1), + throwsAssertionError); + expect( + () => TileOverlay( + tileOverlayId: TileOverlayId('id2'), transparency: 1.2), + throwsAssertionError); + }); + + test('equality', () async { + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: TileOverlayId('id1'), + fadeIn: false, + tileProvider: null, + transparency: 0.1, + zIndex: 1, + visible: false, + tileSize: 128); + final TileOverlay tileOverlay2 = TileOverlay( + tileOverlayId: TileOverlayId('id1'), + fadeIn: false, + tileProvider: null, + transparency: 0.1, + zIndex: 1, + visible: false, + tileSize: 128); + final TileOverlay tileOverlay3 = TileOverlay( + tileOverlayId: TileOverlayId('id2'), + fadeIn: false, + tileProvider: null, + transparency: 0.1, + zIndex: 1, + visible: false, + tileSize: 128); + expect(tileOverlay1, tileOverlay2); + expect(tileOverlay1, isNot(tileOverlay3)); + }); + + test('hashCode', () async { + TileOverlayId id = TileOverlayId('id1'); + final TileOverlay tileOverlay = TileOverlay( + tileOverlayId: id, + fadeIn: false, + tileProvider: null, + transparency: 0.1, + zIndex: 1, + visible: false, + tileSize: 128); + expect( + tileOverlay.hashCode, + hashValues( + tileOverlay.tileOverlayId, + tileOverlay.fadeIn, + tileOverlay.transparency, + tileOverlay.zIndex, + tileOverlay.visible, + tileOverlay.tileSize)); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart new file mode 100644 index 000000000000..f622ca5213ef --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart @@ -0,0 +1,125 @@ +// 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:ui' show hashValues, hashList; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/utils/tile_overlay.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/tile_overlay_updates.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/tile_overlay.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('tile overlay updates tests', () { + test('Correctly set toRemove, toAdd and toChange', () async { + final TileOverlay to1 = TileOverlay(tileOverlayId: TileOverlayId('id1')); + final TileOverlay to2 = TileOverlay(tileOverlayId: TileOverlayId('id2')); + final TileOverlay to3 = TileOverlay(tileOverlayId: TileOverlayId('id3')); + final TileOverlay to3Changed = + TileOverlay(tileOverlayId: TileOverlayId('id3'), transparency: 0.5); + final TileOverlay to4 = TileOverlay(tileOverlayId: TileOverlayId('id4')); + final Set previous = Set.from([to1, to2, to3]); + final Set current = + Set.from([to2, to3Changed, to4]); + final TileOverlayUpdates updates = + TileOverlayUpdates.from(previous, current); + + final Set toRemove = + Set.from([TileOverlayId('id1')]); + expect(updates.tileOverlayIdsToRemove, toRemove); + + final Set toAdd = Set.from([to4]); + expect(updates.tileOverlaysToAdd, toAdd); + + final Set toChange = Set.from([to3Changed]); + expect(updates.tileOverlaysToChange, toChange); + }); + + test('toJson', () async { + final TileOverlay to1 = TileOverlay(tileOverlayId: TileOverlayId('id1')); + final TileOverlay to2 = TileOverlay(tileOverlayId: TileOverlayId('id2')); + final TileOverlay to3 = TileOverlay(tileOverlayId: TileOverlayId('id3')); + final TileOverlay to3Changed = + TileOverlay(tileOverlayId: TileOverlayId('id3'), transparency: 0.5); + final TileOverlay to4 = TileOverlay(tileOverlayId: TileOverlayId('id4')); + final Set previous = Set.from([to1, to2, to3]); + final Set current = + Set.from([to2, to3Changed, to4]); + final TileOverlayUpdates updates = + TileOverlayUpdates.from(previous, current); + + final Object json = updates.toJson(); + expect(json, { + 'tileOverlaysToAdd': serializeTileOverlaySet(updates.tileOverlaysToAdd), + 'tileOverlaysToChange': + serializeTileOverlaySet(updates.tileOverlaysToChange), + 'tileOverlayIdsToRemove': updates.tileOverlayIdsToRemove + .map((TileOverlayId m) => m.value) + .toList() + }); + }); + + test('equality', () async { + final TileOverlay to1 = TileOverlay(tileOverlayId: TileOverlayId('id1')); + final TileOverlay to2 = TileOverlay(tileOverlayId: TileOverlayId('id2')); + final TileOverlay to3 = TileOverlay(tileOverlayId: TileOverlayId('id3')); + final TileOverlay to3Changed = + TileOverlay(tileOverlayId: TileOverlayId('id3'), transparency: 0.5); + final TileOverlay to4 = TileOverlay(tileOverlayId: TileOverlayId('id4')); + final Set previous = Set.from([to1, to2, to3]); + final Set current1 = + Set.from([to2, to3Changed, to4]); + final Set current2 = + Set.from([to2, to3Changed, to4]); + final Set current3 = Set.from([to2, to4]); + final TileOverlayUpdates updates1 = + TileOverlayUpdates.from(previous, current1); + final TileOverlayUpdates updates2 = + TileOverlayUpdates.from(previous, current2); + final TileOverlayUpdates updates3 = + TileOverlayUpdates.from(previous, current3); + expect(updates1, updates2); + expect(updates1, isNot(updates3)); + }); + + test('hashCode', () async { + final TileOverlay to1 = TileOverlay(tileOverlayId: TileOverlayId('id1')); + final TileOverlay to2 = TileOverlay(tileOverlayId: TileOverlayId('id2')); + final TileOverlay to3 = TileOverlay(tileOverlayId: TileOverlayId('id3')); + final TileOverlay to3Changed = + TileOverlay(tileOverlayId: TileOverlayId('id3'), transparency: 0.5); + final TileOverlay to4 = TileOverlay(tileOverlayId: TileOverlayId('id4')); + final Set previous = Set.from([to1, to2, to3]); + final Set current = + Set.from([to2, to3Changed, to4]); + final TileOverlayUpdates updates = + TileOverlayUpdates.from(previous, current); + expect( + updates.hashCode, + hashValues( + hashList(updates.tileOverlaysToAdd), + hashList(updates.tileOverlayIdsToRemove), + hashList(updates.tileOverlaysToChange))); + }); + + test('toString', () async { + final TileOverlay to1 = TileOverlay(tileOverlayId: TileOverlayId('id1')); + final TileOverlay to2 = TileOverlay(tileOverlayId: TileOverlayId('id2')); + final TileOverlay to3 = TileOverlay(tileOverlayId: TileOverlayId('id3')); + final TileOverlay to3Changed = + TileOverlay(tileOverlayId: TileOverlayId('id3'), transparency: 0.5); + final TileOverlay to4 = TileOverlay(tileOverlayId: TileOverlayId('id4')); + final Set previous = Set.from([to1, to2, to3]); + final Set current = + Set.from([to2, to3Changed, to4]); + final TileOverlayUpdates updates = + TileOverlayUpdates.from(previous, current); + expect( + updates.toString(), + 'TileOverlayUpdates(add: ${updates.tileOverlaysToAdd}, ' + 'remove: ${updates.tileOverlayIdsToRemove}, ' + 'change: ${updates.tileOverlaysToChange})'); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart new file mode 100644 index 000000000000..3e0fe99ec18c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart @@ -0,0 +1,33 @@ +// 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:typed_data'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('tile tests', () { + test('toJson returns correct format', () async { + final Uint8List data = Uint8List.fromList([0, 1]); + final Tile tile = Tile(100, 200, data); + final Object json = tile.toJson(); + expect(json, { + 'width': 100, + 'height': 200, + 'data': data, + }); + }); + + test('toJson handles null data', () async { + final Tile tile = Tile(0, 0, null); + final Object json = tile.toJson(); + expect(json, { + 'width': 0, + 'height': 0, + }); + }); + }); +} 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 07700151a2d1..f5e289cb19ae 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,25 @@ +## 0.1.2 + +* Add support for Custom Tiles. + +## 0.1.1 + +* Auto-reverse holes if they're the same direction as the polygon. [Issue](https://github.com/flutter/flutter/issues/74096). + +## 0.1.0+10 + +* Update `package:google_maps_flutter_platform_interface` to `^1.1.0`. +* Add support for Polygon Holes. + +## 0.1.0+9 + +* Update Flutter SDK constraint. + +## 0.1.0+8 + +* Update `package:google_maps_flutter_platform_interface` to `^1.0.5`. +* Add support for `fromBitmap` BitmapDescriptors. [Issue](https://github.com/flutter/flutter/issues/66622). + ## 0.1.0+7 * 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/google_maps_flutter/google_maps_flutter_web/ios/google_maps_flutter_web.podspec b/packages/google_maps_flutter/google_maps_flutter_web/ios/google_maps_flutter_web.podspec deleted file mode 100644 index 18db6ced01b6..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/ios/google_maps_flutter_web.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint google_maps_flutter_web.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'google_maps_flutter_web' - s.version = '0.1.0' - s.summary = 'No-op implementation of google maps flutter web plugin to avoid build issues on iOS' - s.description = <<-DESC -temp fake google_maps_flutter_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart index e8847fdd7b84..95f481a9bdc5 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart @@ -264,6 +264,7 @@ Set _rawOptionsToInitialMarkers(Map rawOptions) { Offset offset; LatLng position; InfoWindow infoWindow; + BitmapDescriptor icon; if (rawMarker['anchor'] != null) { offset = Offset((rawMarker['anchor'][0]), (rawMarker['anchor'][1])); } @@ -280,6 +281,9 @@ Set _rawOptionsToInitialMarkers(Map rawOptions) { ); } } + if (rawMarker['icon'] != null) { + icon = BitmapDescriptor.fromJson(rawMarker['icon']); + } return Marker( markerId: MarkerId(rawMarker['markerId']), alpha: rawMarker['alpha'], @@ -287,8 +291,7 @@ Set _rawOptionsToInitialMarkers(Map rawOptions) { consumeTapEvents: rawMarker['consumeTapEvents'], draggable: rawMarker['draggable'], flat: rawMarker['flat'], - // TODO: Doesn't this support custom icons? - icon: BitmapDescriptor.defaultMarker, + icon: icon, infoWindow: infoWindow, position: position ?? _nullLatLng, rotation: rawMarker['rotation'], @@ -363,6 +366,11 @@ Set _rawOptionsToInitialPolygons(Map rawOptions) { points: rawPolygon['points'] ?.map((rawPoint) => LatLng.fromJson(rawPoint)) ?.toList(), + holes: rawPolygon['holes'] + ?.map>((List hole) => hole + ?.map((rawPoint) => LatLng.fromJson(rawPoint)) + ?.toList()) + ?.toList(), ); }) ?? []); @@ -432,6 +440,11 @@ gmaps.MarkerOptions _markerOptionsFromMarker( ..size = size ..scaledSize = size; } + } else if (iconConfig[0] == 'fromBytes') { + // Grab the bytes, and put them into a blob + List bytes = iconConfig[1]; + final blob = Blob([bytes]); // Let the browser figure out the encoding + icon = gmaps.Icon()..url = Url.createObjectUrlFromBlob(blob); } } return gmaps.MarkerOptions() @@ -465,9 +478,27 @@ gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { gmaps.PolygonOptions _polygonOptionsFromPolygon( gmaps.GMap googleMap, Polygon polygon) { - List paths = []; + List path = []; polygon.points.forEach((point) { - paths.add(_latLngToGmLatLng(point)); + path.add(_latLngToGmLatLng(point)); + }); + final polygonDirection = _isPolygonClockwise(path); + List> paths = [path]; + int holeIndex = 0; + polygon.holes?.forEach((hole) { + List holePath = + hole.map((point) => _latLngToGmLatLng(point)).toList(); + if (_isPolygonClockwise(holePath) == polygonDirection) { + holePath = holePath.reversed.toList(); + if (kDebugMode) { + print( + 'Hole [$holeIndex] in Polygon [${polygon.polygonId.value}] has been reversed.' + ' Ensure holes in polygons are "wound in the opposite direction to the outer path."' + ' More info: https://github.com/flutter/flutter/issues/74096'); + } + } + paths.add(holePath); + holeIndex++; }); return gmaps.PolygonOptions() ..paths = paths @@ -481,6 +512,20 @@ gmaps.PolygonOptions _polygonOptionsFromPolygon( ..geodesic = polygon.geodesic; } +/// Calculates the direction of a given Polygon +/// based on: https://stackoverflow.com/a/1165943 +/// +/// returns [true] if clockwise [false] if counterclockwise +bool _isPolygonClockwise(List path) { + var direction = 0.0; + for (var i = 0; i < path.length; i++) { + direction = direction + + ((path[(i + 1) % path.length].lat - path[i].lat) * + (path[(i + 1) % path.length].lng + path[i].lng)); + } + return direction >= 0; +} + gmaps.PolylineOptions _polylineOptionsFromPolyline( gmaps.GMap googleMap, Polyline polyline) { List paths = []; 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 bbbfcfa79472..83ac0fbdde5f 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+7 +version: 0.1.2 flutter: plugin: @@ -16,7 +16,7 @@ dependencies: flutter_web_plugins: sdk: flutter meta: ^1.1.7 - google_maps_flutter_platform_interface: ^1.0.4 + google_maps_flutter_platform_interface: ^1.1.0 google_maps: ^3.4.5 stream_transform: ^1.2.0 sanitize_html: ^1.4.1 @@ -24,6 +24,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + http: ^0.12.2 url_launcher: ^5.2.5 pedantic: ^1.8.0 mockito: ^4.1.1 @@ -32,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_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_integration.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_integration.dart index dfd01c6faf55..302057f0ea57 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_integration.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_integration.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:async'; import 'package:integration_test/integration_test.dart'; @@ -164,6 +166,22 @@ void main() { [43.354762, -5.850824], ], }, + { + 'polygonId': 'polygon-2-with-holes', + 'points': [ + [43.355114, -5.851333], + [43.354797, -5.851860], + [43.354469, -5.851318], + [43.354762, -5.850824], + ], + 'holes': [ + [ + [41.354797, -6.851860], + [41.354469, -6.851318], + [41.354762, -6.850824], + ] + ] + }, ], 'polylinesToAdd': [ { @@ -200,6 +218,9 @@ void main() { expect(capturedMarkers.first.infoWindow.snippet, 'snippet for test'); expect(capturedMarkers.first.infoWindow.title, 'title for test'); expect(capturedPolygons.first.polygonId.value, 'polygon-1'); + expect(capturedPolygons.elementAt(1).polygonId.value, + 'polygon-2-with-holes'); + expect(capturedPolygons.elementAt(1).holes, isNot(null)); expect(capturedPolylines.first.polylineId.value, 'polyline-1'); }); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_integration.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_integration.dart index 59b5de42400f..6276d26753a4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_integration.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_integration.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:async'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_integration.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_integration.dart index 1cada32104af..c83e92dbb79d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_integration.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_integration.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:async'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_integration.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_integration.dart index cad8cd8acfed..9c3ed8ea3860 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_integration.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_integration.dart @@ -2,14 +2,20 @@ // 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:html'; +import 'package:http/http.dart' as http; import 'package:integration_test/integration_test.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'resources/icon_image_base64.dart'; + void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -141,6 +147,30 @@ void main() { expect(controller.markers[MarkerId('1')].marker.icon, isNull); }); + // + testWidgets('markers with custom bitmap icon work', + (WidgetTester tester) async { + final bytes = Base64Decoder().convert(iconImageBase64); + final markers = { + Marker( + markerId: MarkerId('1'), icon: BitmapDescriptor.fromBytes(bytes)), + }; + + controller.addMarkers(markers); + + expect(controller.markers.length, 1); + expect(controller.markers[MarkerId('1')].marker.icon, isNotNull); + expect(controller.markers[MarkerId('1')].marker.icon.url, + startsWith('blob:')); + + final blobUrl = controller.markers[MarkerId('1')].marker.icon.url; + final response = await http.get(blobUrl); + + expect(response.bodyBytes, bytes, + reason: + 'Bytes from the Icon blob must match bytes used to create Marker'); + }); + // https://github.com/flutter/flutter/issues/67854 testWidgets('InfoWindow snippet can have links', (WidgetTester tester) async { diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/resources/icon_image_base64.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/resources/icon_image_base64.dart new file mode 100644 index 000000000000..aa4a80baddbb --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/resources/icon_image_base64.dart @@ -0,0 +1,24 @@ +final iconImageBase64 = + 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAIRlWElmTU' + '0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIA' + 'AIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQ' + 'AAABCgAwAEAAAAAQAAABAAAAAAx28c8QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1M' + 'OmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIH' + 'g6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8v' + 'd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcm' + 'lwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFk' + 'b2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk' + '9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6' + 'eG1wbWV0YT4KTMInWQAAAplJREFUOBF1k01ME1EQx2fe7tIPoGgTE6AJgQQSPaiH9oAtkFbsgX' + 'jygFcT0XjSkxcTDxtPJh6MR28ePMHBBA8cNLSIony0oBhEMVETP058tE132+7uG3cW24DAXN57' + '2fn9/zPz3iIcEdEl0nIxtNLr1IlVeoMadkubKmoL+u2SzAV8IjV5Ekt4GN+A8+VOUPwLarOI2G' + 'Vpqq0i4JQorwQxPtWHVZ1IKP8LNGDXGaSyqARFxDGo7MJBy4XVf3AyQ+qTHnTEXoF9cFUy3OkY' + '0oWxmWFtD5xNoc1sQ6AOn1+hCNTkkhKow8KFZV77tVs2O9dhFvBm0IA/U0RhZ7/ocEx23oUDlh' + 'h8HkNjZIN8Lb3gOU8gOp7AKJHCB2/aNZkTftHumNzzbtl2CBPZHqxw8mHhVZBeoz6w5DvhE2FZ' + 'lQYPjKdd2/qRyKZ6KsPv7TEk7EYEk0A0EUmJduHRy1i4oLKqgmC59ZggAdwrC9pFuWy1iUT2rA' + 'uv0h2UdNtNqxCBBkgqorjOMOgksN7CxQ90vEb00U3c3LIwyo9o8FXxQVNr8Coqyk+S5EPBXnjt' + 'xRmc4TegI7qWbvBkeeUbGMnTCd4nZnYeDOWIEtlC6cKK/JJepY3hZSvN33jovO6L0XFqPKqBTO' + 'FuapUoPr1lxDM7cmC2TAOz25cYSGa++feBew/cjpc0V+mNT29/HZp3KDFTNLvuTRPEHy5065lj' + 'Xn4y41XM+wP/AlcycRmdc3MUhvLm/J/ceu/3qUVT62oP2EZpjSylHybHSpDUVcjq9gEBVo0+Xt' + 'JyN2IWRO+3QUforRoKnZLVsglaMECW+YmMSj9M3SrC6Lg71CMiqWfUrJ6ywzefhnZ+G69BaKdB' + 'WhXQAn6wzDUpfUPw7MrmX/WhbfmKblw+AAAAAElFTkSuQmCC'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_integration.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_integration.dart index a05d704850e6..b1a691d18def 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_integration.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_integration.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:async'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_integration.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_integration.dart index 4345843eb537..26db542c60fe 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_integration.dart +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_integration.dart @@ -2,12 +2,16 @@ // 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:ui'; import 'package:integration_test/integration_test.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps/google_maps_geometry.dart' as geometry; import 'package:flutter_test/flutter_test.dart'; // This value is used when comparing the results of @@ -188,6 +192,92 @@ void main() { expect(polygon.get('strokeColor'), '#c0ffee'); expect(polygon.get('strokeOpacity'), closeTo(1, _acceptableDelta)); }); + + testWidgets('Handle Polygons with holes', (WidgetTester tester) async { + final polygons = { + Polygon( + polygonId: PolygonId('BermudaTriangle'), + points: [ + LatLng(25.774, -80.19), + LatLng(18.466, -66.118), + LatLng(32.321, -64.757), + ], + holes: [ + [ + LatLng(28.745, -70.579), + LatLng(29.57, -67.514), + LatLng(27.339, -66.668), + ], + ], + ), + }; + + controller.addPolygons(polygons); + + expect(controller.polygons.length, 1); + expect(controller.polygons, contains(PolygonId('BermudaTriangle'))); + expect(controller.polygons, isNot(contains(PolygonId('66')))); + }); + + testWidgets('Polygon with hole has a hole', (WidgetTester tester) async { + final polygons = { + Polygon( + polygonId: PolygonId('BermudaTriangle'), + points: [ + LatLng(25.774, -80.19), + LatLng(18.466, -66.118), + LatLng(32.321, -64.757), + ], + holes: [ + [ + LatLng(28.745, -70.579), + LatLng(29.57, -67.514), + LatLng(27.339, -66.668), + ], + ], + ), + }; + + controller.addPolygons(polygons); + + final polygon = controller.polygons.values.first.polygon; + final pointInHole = gmaps.LatLng(28.632, -68.401); + + expect(geometry.poly.containsLocation(pointInHole, polygon), false); + }); + + testWidgets('Hole Path gets reversed to display correctly', + (WidgetTester tester) async { + final polygons = { + Polygon( + polygonId: PolygonId('BermudaTriangle'), + points: [ + LatLng(25.774, -80.19), + LatLng(18.466, -66.118), + LatLng(32.321, -64.757), + ], + holes: [ + [ + LatLng(27.339, -66.668), + LatLng(29.57, -67.514), + LatLng(28.745, -70.579), + ], + ], + ), + }; + + controller.addPolygons(polygons); + + expect( + controller.polygons.values.first.polygon.paths.getAt(1).getAt(0).lat, + 28.745); + expect( + controller.polygons.values.first.polygon.paths.getAt(1).getAt(1).lat, + 29.57); + expect( + controller.polygons.values.first.polygon.paths.getAt(1).getAt(2).lat, + 27.339); + }); }); group('PolylinesController', () { diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/web/index.html b/packages/google_maps_flutter/google_maps_flutter_web/test/web/index.html index 3b7e4edc3df1..03feba172d74 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/web/index.html +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/web/index.html @@ -5,7 +5,7 @@ Browser Tests - + 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..5e29f340599b 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,20 @@ +## 2.0.0 + +* Migrate to null safety. +* Fixes the requested scopes to use the `GoogleSignIn` instance's `scopes`. + +## 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..0ec62a832648 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 @@ -56,7 +56,8 @@ class SignInDemoState extends State { _contactText = 'Loading contact info...'; }); - final peopleApi = PeopleApi(await _googleSignIn.authenticatedClient()); + final peopleApi = + PeopleServiceApi(await _googleSignIn.authenticatedClient()); final response = await peopleApi.people.connections.list( 'people/me', personFields: 'names', @@ -111,11 +112,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 +127,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/example/pubspec.yaml b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml index 3e64fa2da3f2..d3b428d190c9 100755 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml +++ b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml @@ -4,13 +4,18 @@ description: Example of Google Sign-In plugin and googleapis. dependencies: flutter: sdk: flutter - google_sign_in: ^4.4.1 + google_sign_in: ^5.0.0 extension_google_sign_in_as_googleapis_auth: + # When depending on this package from a real application you should use: + # extension_google_sign_in_as_googleapis_auth: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - googleapis: ^0.55.0 + googleapis: ^1.0.0 dev_dependencies: - pedantic: ^1.8.0 + pedantic: ^1.10.0 integration_test: path: ../../../integration_test flutter_driver: @@ -21,4 +26,4 @@ flutter: environment: sdk: ">=2.0.0-dev.28.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/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart index eec45cc0e89a..8c8ede5eee1a 100644 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart +++ b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart @@ -15,15 +15,20 @@ import 'package:http/http.dart' as http; /// client that can be used with the rest of the `googleapis` libraries. extension GoogleApisGoogleSignInAuth on GoogleSignIn { /// Retrieve a `googleapis` authenticated client. - Future authenticatedClient({ - @visibleForTesting GoogleSignInAuthentication debugAuthentication, - @visibleForTesting List debugScopes = const [], + Future authenticatedClient({ + @visibleForTesting GoogleSignInAuthentication? debugAuthentication, + @visibleForTesting List? debugScopes, }) async { - final auth = debugAuthentication ?? await currentUser.authentication; + final GoogleSignInAuthentication? auth = + debugAuthentication ?? await currentUser?.authentication; + final String? oathTokenString = auth?.accessToken; + if (oathTokenString == null) { + return null; + } final credentials = googleapis_auth.AccessCredentials( googleapis_auth.AccessToken( 'Bearer', - auth.accessToken, + oathTokenString, // We don't know when the token expires, so we assume "never" DateTime.now().toUtc().add(Duration(days: 365)), ), 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..7d86b67196d0 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,23 +6,23 @@ 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: 2.0.0 homepage: https://github.com/flutter/plugins/google_sign_in/extension_google_sign_in_as_googleapis_auth dependencies: flutter: sdk: flutter - google_sign_in: ^4.4.1 - googleapis_auth: ^0.2.11+1 - meta: ^1.1.8 - http: ^0.12.1 + google_sign_in: ^5.0.0 + googleapis_auth: ^1.0.0 + meta: ^1.3.0 + http: ^0.13.0 dev_dependencies: - mockito: ^4.1.1 - pedantic: ^1.9.0 + pedantic: ^1.10.0 + test: ^1.16.3 flutter_test: sdk: flutter environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.4" diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart index 9f86703d4bb8..508f366eacde 100644 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart +++ b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart @@ -7,25 +7,25 @@ import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis_auth/auth.dart' as auth; import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart'; -import 'package:mockito/mockito.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:test/fake.dart'; -// Mocks so I don't have to prepare all the GoogleSignIn environment. -class MockGoogleSignIn extends Mock implements GoogleSignIn {} +const SOME_FAKE_ACCESS_TOKEN = 'this-is-something-not-null'; +const DEBUG_FAKE_SCOPES = ['some-scope', 'another-scope']; +const SIGN_IN_FAKE_SCOPES = ['some-scope', 'another-scope']; -class MockGoogleSignInAuthentication extends Mock - implements GoogleSignInAuthentication {} +class FakeGoogleSignIn extends Fake implements GoogleSignIn { + final List scopes = SIGN_IN_FAKE_SCOPES; +} -const SOME_FAKE_ACCESS_TOKEN = 'this-is-something-not-null'; -const SOME_FAKE_SCOPES = ['some-scope', 'another-scope']; +class FakeGoogleSignInAuthentication extends Fake + implements GoogleSignInAuthentication { + final String accessToken = SOME_FAKE_ACCESS_TOKEN; +} void main() { - GoogleSignIn signIn = MockGoogleSignIn(); - final authMock = MockGoogleSignInAuthentication(); - - setUp(() { - when(authMock.accessToken).thenReturn(SOME_FAKE_ACCESS_TOKEN); - }); + GoogleSignIn signIn = FakeGoogleSignIn(); + final authMock = FakeGoogleSignInAuthentication(); test('authenticatedClient returns an authenticated client', () async { final client = await signIn.authenticatedClient( @@ -34,13 +34,21 @@ void main() { expect(client, isA()); }); + test('authenticatedClient uses GoogleSignIn scopes by default', () async { + final client = (await signIn.authenticatedClient( + debugAuthentication: authMock, + ))!; + expect(client.credentials.accessToken.data, equals(SOME_FAKE_ACCESS_TOKEN)); + expect(client.credentials.scopes, equals(SIGN_IN_FAKE_SCOPES)); + }); + test('authenticatedClient returned client contains the passed-in credentials', () async { - final client = await signIn.authenticatedClient( + final client = (await signIn.authenticatedClient( debugAuthentication: authMock, - debugScopes: SOME_FAKE_SCOPES, - ); + debugScopes: DEBUG_FAKE_SCOPES, + ))!; expect(client.credentials.accessToken.data, equals(SOME_FAKE_ACCESS_TOKEN)); - expect(client.credentials.scopes, equals(SOME_FAKE_SCOPES)); + expect(client.credentials.scopes, equals(DEBUG_FAKE_SCOPES)); }); } diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index c8fc7ecea349..57d1c9be3743 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,6 +1,18 @@ +## 5.0.0 + +* Migrate to null safety. + +## 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 -* Fix flutter/flutter#48602 iOS flow shows account selection, if user is signed in to Google on the device. +* Update Flutter SDK constraint. ## 4.5.6 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..e003225af5cc 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 @@ -33,31 +33,31 @@ class SignInDemo extends StatefulWidget { } class SignInDemoState extends State { - GoogleSignInAccount _currentUser; - String _contactText; + GoogleSignInAccount? _currentUser; + String _contactText = ''; @override void initState() { super.initState(); - _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount account) { + _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount? account) { setState(() { _currentUser = account; }); if (_currentUser != null) { - _handleGetContact(); + _handleGetContact(_currentUser!); } }); _googleSignIn.signInSilently(); } - Future _handleGetContact() async { + Future _handleGetContact(GoogleSignInAccount user) async { setState(() { _contactText = "Loading contact info..."; }); final http.Response response = await http.get( - 'https://people.googleapis.com/v1/people/me/connections' - '?requestMask.includeField=person.names', - headers: await _currentUser.authHeaders, + Uri.parse('https://people.googleapis.com/v1/people/me/connections' + '?requestMask.includeField=person.names'), + headers: await user.authHeaders, ); if (response.statusCode != 200) { setState(() { @@ -68,7 +68,7 @@ class SignInDemoState extends State { return; } final Map data = json.decode(response.body); - final String namedContact = _pickFirstNamedContact(data); + final String? namedContact = _pickFirstNamedContact(data); setState(() { if (namedContact != null) { _contactText = "I see you know $namedContact!"; @@ -78,14 +78,14 @@ class SignInDemoState extends State { }); } - String _pickFirstNamedContact(Map data) { - final List connections = data['connections']; - final Map contact = connections?.firstWhere( + String? _pickFirstNamedContact(Map data) { + final List? connections = data['connections']; + final Map? contact = connections?.firstWhere( (dynamic contact) => contact['names'] != null, orElse: () => null, ); if (contact != null) { - final Map name = contact['names'].firstWhere( + final Map? name = contact['names'].firstWhere( (dynamic name) => name['displayName'] != null, orElse: () => null, ); @@ -107,26 +107,27 @@ class SignInDemoState extends State { Future _handleSignOut() => _googleSignIn.disconnect(); Widget _buildBody() { - if (_currentUser != null) { + GoogleSignInAccount? user = _currentUser; + if (user != null) { return Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ListTile( leading: GoogleUserCircleAvatar( - identity: _currentUser, + identity: user, ), - title: Text(_currentUser.displayName ?? ''), - subtitle: Text(_currentUser.email ?? ''), + title: Text(user.displayName ?? ''), + subtitle: Text(user.email), ), const Text("Signed in successfully."), - Text(_contactText ?? ''), - RaisedButton( + Text(_contactText), + ElevatedButton( child: const Text('SIGN OUT'), onPressed: _handleSignOut, ), - RaisedButton( + ElevatedButton( child: const Text('REFRESH'), - onPressed: _handleGetContact, + onPressed: () => _handleGetContact(user), ), ], ); @@ -135,7 +136,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/example/pubspec.yaml b/packages/google_sign_in/google_sign_in/example/pubspec.yaml index ebf8e82719f2..b5a1f3e1c2cc 100755 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -5,11 +5,16 @@ dependencies: flutter: sdk: flutter google_sign_in: + # When depending on this package from a real application you should use: + # google_sign_in: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - http: ^0.12.0 + http: ^0.13.0 dev_dependencies: - pedantic: ^1.8.0 + pedantic: ^1.10.0 integration_test: path: ../../../integration_test flutter_driver: @@ -19,5 +24,5 @@ flutter: uses-material-design: true environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.4" diff --git a/packages/google_sign_in/google_sign_in/integration_test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/integration_test/google_sign_in_test.dart index 7b2b8d800778..a900bfbfdc2e 100644 --- a/packages/google_sign_in/google_sign_in/integration_test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/integration_test/google_sign_in_test.dart @@ -1,3 +1,5 @@ +// @dart = 2.9 + import 'package:integration_test/integration_test.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in/google_sign_in.dart'; diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 0f1f15bbb8c4..1317eb91ecfb 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -22,13 +22,13 @@ class GoogleSignInAuthentication { final GoogleSignInTokenData _data; /// An OpenID Connect ID token that identifies the user. - String get idToken => _data.idToken; + String? get idToken => _data.idToken; /// The OAuth2 access token to access Google services. - String get accessToken => _data.accessToken; + String? get accessToken => _data.accessToken; /// Server auth code used to access Google Login - String get serverAuthCode => _data.serverAuthCode; + String? get serverAuthCode => _data.serverAuthCode; @override String toString() => 'GoogleSignInAuthentication:$_data'; @@ -57,7 +57,7 @@ class GoogleSignInAccount implements GoogleIdentity { static const String kUserRecoverableAuthError = 'user_recoverable_auth'; @override - final String displayName; + final String? displayName; @override final String email; @@ -66,9 +66,9 @@ class GoogleSignInAccount implements GoogleIdentity { final String id; @override - final String photoUrl; + final String? photoUrl; - final String _idToken; + final String? _idToken; final GoogleSignIn _googleSignIn; /// Retrieve [GoogleSignInAuthentication] for this account. @@ -105,7 +105,7 @@ class GoogleSignInAccount implements GoogleIdentity { /// /// See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization. Future> get authHeaders async { - final String token = (await authentication).accessToken; + final String? token = (await authentication).accessToken; return { "Authorization": "Bearer $token", "X-Goog-AuthUser": "0", @@ -117,7 +117,7 @@ class GoogleSignInAccount implements GoogleIdentity { /// If client runs into 401 errors using a token, it is expected to call /// this method and grab `authHeaders` once again. Future clearAuthCache() async { - final String token = (await authentication).accessToken; + final String token = (await authentication).accessToken!; await GoogleSignInPlatform.instance.clearAuthCache(token: token); } @@ -174,7 +174,7 @@ class GoogleSignIn { /// Factory for creating default sign in user experience. factory GoogleSignIn.standard({ List scopes = const [], - String hostedDomain, + String? hostedDomain, }) { return GoogleSignIn( signInOption: SignInOption.standard, @@ -212,22 +212,22 @@ class GoogleSignIn { final List scopes; /// Domain to restrict sign-in to. - final String hostedDomain; + final String? hostedDomain; /// Client ID being used to connect to google sign-in. Only supported on web. - final String clientId; + final String? clientId; - StreamController _currentUserController = - StreamController.broadcast(); + StreamController _currentUserController = + StreamController.broadcast(); /// Subscribe to this stream to be notified when the current user changes. - Stream get onCurrentUserChanged => + Stream get onCurrentUserChanged => _currentUserController.stream; // Future that completes when we've finished calling `init` on the native side - Future _initialization; + Future? _initialization; - Future _callMethod(Function method) async { + Future _callMethod(Function method) async { await _ensureInitialized(); final dynamic response = await method(); @@ -237,7 +237,7 @@ class GoogleSignIn { : null); } - GoogleSignInAccount _setCurrentUser(GoogleSignInAccount currentUser) { + GoogleSignInAccount? _setCurrentUser(GoogleSignInAccount? currentUser) { if (currentUser != _currentUser) { _currentUser = currentUser; _currentUserController.add(_currentUser); @@ -258,7 +258,7 @@ class GoogleSignIn { } /// The most recently scheduled method call. - Future _lastMethodCall; + Future? _lastMethodCall; /// Returns a [Future] that completes with a success after [future], whether /// it completed with a value or an error. @@ -279,15 +279,15 @@ class GoogleSignIn { /// The optional, named parameter [canSkipCall] lets the plugin know that the /// method call may be skipped, if there's already [_currentUser] information. /// This is used from the [signIn] and [signInSilently] methods. - Future _addMethodCall( + Future _addMethodCall( Function method, { bool canSkipCall = false, }) async { - Future response; + Future response; if (_lastMethodCall == null) { response = _callMethod(method); } else { - response = _lastMethodCall.then((_) { + response = _lastMethodCall!.then((_) { // If after the last completed call `currentUser` is not `null` and requested // method can be skipped (`canSkipCall`), re-use the same authenticated user // instead of making extra call to the native side. @@ -303,8 +303,8 @@ class GoogleSignIn { } /// The currently signed in account, or null if the user is signed out. - GoogleSignInAccount get currentUser => _currentUser; - GoogleSignInAccount _currentUser; + GoogleSignInAccount? get currentUser => _currentUser; + GoogleSignInAccount? _currentUser; /// Attempts to sign in a previously authenticated user without interaction. /// @@ -323,7 +323,7 @@ class GoogleSignIn { /// one of [kSignInRequiredError] (when there is no authenticated user) , /// [kNetworkError] (when a network error occurred) or [kSignInFailedError] /// (when an unknown error occurred). - Future signInSilently({ + Future signInSilently({ bool suppressErrors = true, }) async { try { @@ -354,8 +354,8 @@ class GoogleSignIn { /// a Future which resolves to the same user instance. /// /// Re-authentication can be triggered only after [signOut] or [disconnect]. - Future signIn() { - final Future result = + Future signIn() { + final Future result = _addMethodCall(GoogleSignInPlatform.instance.signIn, canSkipCall: true); bool isCanceled(dynamic error) => error is PlatformException && error.code == kSignInCanceledError; @@ -363,12 +363,12 @@ class GoogleSignIn { } /// Marks current user as being in the signed out state. - Future signOut() => + Future signOut() => _addMethodCall(GoogleSignInPlatform.instance.signOut); /// Disconnects the current user from the app and revokes previous /// authentication. - Future disconnect() => + Future disconnect() => _addMethodCall(GoogleSignInPlatform.instance.disconnect); /// Requests the user grants additional Oauth [scopes]. diff --git a/packages/google_sign_in/google_sign_in/lib/src/common.dart b/packages/google_sign_in/google_sign_in/lib/src/common.dart index 14bed4fe114a..60c74ab4c6d5 100644 --- a/packages/google_sign_in/google_sign_in/lib/src/common.dart +++ b/packages/google_sign_in/google_sign_in/lib/src/common.dart @@ -28,10 +28,10 @@ abstract class GoogleIdentity { /// The display name of the signed in user. /// /// Not guaranteed to be present for all users, even when configured. - String get displayName; + String? get displayName; /// The photo url of the signed in user if the user has a profile picture. /// /// Not guaranteed to be present for all users, even when configured. - String get photoUrl; + String? get photoUrl; } diff --git a/packages/google_sign_in/google_sign_in/lib/testing.dart b/packages/google_sign_in/google_sign_in/lib/testing.dart index 8d62ff463ca6..ed0ca4f2de7c 100644 --- a/packages/google_sign_in/google_sign_in/lib/testing.dart +++ b/packages/google_sign_in/google_sign_in/lib/testing.dart @@ -32,7 +32,7 @@ class FakeSignInBackend { /// This does not represent the signed-in user, but rather an object that will /// be returned when [GoogleSignIn.signIn] or [GoogleSignIn.signInSilently] is /// called. - FakeUser user; + late FakeUser user; /// Handles method calls that would normally be sent to the native backend. /// Returns with the expected values based on the current [user]. @@ -42,7 +42,7 @@ class FakeSignInBackend { // do nothing return null; case 'getTokens': - return { + return { 'idToken': user.idToken, 'accessToken': user.accessToken, }; @@ -72,24 +72,24 @@ class FakeUser { }); /// Will be converted into [GoogleSignInUserData.id]. - final String id; + final String? id; /// Will be converted into [GoogleSignInUserData.email]. - final String email; + final String? email; /// Will be converted into [GoogleSignInUserData.displayName]. - final String displayName; + final String? displayName; /// Will be converted into [GoogleSignInUserData.photoUrl]. - final String photoUrl; + final String? photoUrl; /// Will be converted into [GoogleSignInTokenData.idToken]. - final String idToken; + final String? idToken; /// Will be converted into [GoogleSignInTokenData.accessToken]. - final String accessToken; + final String? accessToken; - Map get _asMap => { + Map get _asMap => { 'id': id, 'email': email, 'displayName': displayName, diff --git a/packages/google_sign_in/google_sign_in/lib/widgets.dart b/packages/google_sign_in/google_sign_in/lib/widgets.dart index 3375628f47b5..c9682930b089 100644 --- a/packages/google_sign_in/google_sign_in/lib/widgets.dart +++ b/packages/google_sign_in/google_sign_in/lib/widgets.dart @@ -4,7 +4,6 @@ import 'dart:typed_data'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'src/common.dart'; @@ -23,7 +22,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { /// in place of a profile photo, or a default profile photo if the user's /// identity does not specify a `displayName`. const GoogleUserCircleAvatar({ - @required this.identity, + required this.identity, this.placeholderPhotoUrl, this.foregroundColor, this.backgroundColor, @@ -42,13 +41,13 @@ class GoogleUserCircleAvatar extends StatelessWidget { /// The color of the text to be displayed if photo is not available. /// /// If a foreground color is not specified, the theme's text color is used. - final Color foregroundColor; + final Color? foregroundColor; /// The color with which to fill the circle. Changing the background color /// will cause the avatar to animate to the new color. /// /// If a background color is not specified, the theme's primary color is used. - final Color backgroundColor; + final Color? backgroundColor; /// The URL of a photo to use if the user's [identity] does not specify a /// `photoUrl`. @@ -57,7 +56,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { /// then this widget will attempt to display the user's first initial as /// determined from the identity's [displayName] field. If that is `null` a /// default (generic) Google profile photo will be displayed. - final String placeholderPhotoUrl; + final String? placeholderPhotoUrl; @override Widget build(BuildContext context) { @@ -68,38 +67,26 @@ class GoogleUserCircleAvatar extends StatelessWidget { ); } - /// Adds correct sizing information to [photoUrl]. - /// - /// Falls back to the default profile photo if [photoUrl] is [null]. - static String _sizedProfileImageUrl(String photoUrl, double size) { - if (photoUrl == null) { - // If the user has no profile photo and no display name, fall back to - // the default profile photo as a last resort. - return 'https://lh3.googleusercontent.com/a/default-user=s${size.round()}-c'; - } - return fife.addSizeDirectiveToUrl(photoUrl, size); - } - Widget _buildClippedImage(BuildContext context, BoxConstraints constraints) { assert(constraints.maxWidth == constraints.maxHeight); // Placeholder to use when there is no photo URL, and while the photo is // loading. Uses the first character of the display name (if it has one), // or the first letter of the email address if it does not. - final List placeholderCharSources = [ + final List placeholderCharSources = [ identity.displayName, identity.email, '-', ]; final String placeholderChar = placeholderCharSources - .firstWhere((String str) => str != null && str.trimLeft().isNotEmpty) + .firstWhere((String? str) => str != null && str.trimLeft().isNotEmpty)! .trimLeft()[0] .toUpperCase(); final Widget placeholder = Center( child: Text(placeholderChar, textAlign: TextAlign.center), ); - final String photoUrl = identity.photoUrl ?? placeholderPhotoUrl; + final String? photoUrl = identity.photoUrl ?? placeholderPhotoUrl; if (photoUrl == null) { return placeholder; } @@ -107,7 +94,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { // Add a sizing directive to the profile photo URL. final double size = MediaQuery.of(context).devicePixelRatio * constraints.maxWidth; - final String sizedPhotoUrl = _sizedProfileImageUrl(photoUrl, size); + final String sizedPhotoUrl = fife.addSizeDirectiveToUrl(photoUrl, size); // Fade the photo in over the top of the placeholder. return SizedBox( diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index 5bc632fbda06..06fa12c0f4c0 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.7 +version: 5.0.0 flutter: plugin: @@ -16,27 +16,22 @@ flutter: default_package: google_sign_in_web dependencies: - google_sign_in_platform_interface: ^1.1.1 + google_sign_in_platform_interface: ^2.0.0 + google_sign_in_web: ^0.10.0 flutter: sdk: flutter - meta: ^1.0.4 - # 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 - google_sign_in_web: ^0.9.1 + meta: ^1.3.0 dev_dependencies: - http: ^0.12.0 + http: ^0.13.0 flutter_driver: sdk: flutter flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 integration_test: path: ../../integration_test environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" 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..2a8d65e32ffd 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,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 'package:flutter/services.dart'; @@ -43,8 +41,8 @@ void main() { }; final List log = []; - Map responses; - GoogleSignIn googleSignIn; + late Map responses; + late GoogleSignIn googleSignIn; setUp(() { responses = Map.from(kDefaultResponses); @@ -173,16 +171,16 @@ void main() { }); test('concurrent calls of the same method trigger sign in once', () async { - final List> futures = - >[ + final List> futures = + >[ googleSignIn.signInSilently(), googleSignIn.signInSilently(), ]; expect(futures.first, isNot(futures.last), reason: 'Must return new Future'); - final List users = await Future.wait(futures); + final List users = await Future.wait(futures); expect(googleSignIn.currentUser, isNotNull); - expect(users, [ + expect(users, [ googleSignIn.currentUser, googleSignIn.currentUser ]); @@ -218,13 +216,13 @@ void main() { }); test('concurrent calls of different signIn methods', () async { - final List> futures = - >[ + final List> futures = + >[ googleSignIn.signInSilently(), googleSignIn.signIn(), ]; expect(futures.first, isNot(futures.last)); - final List users = await Future.wait(futures); + final List users = await Future.wait(futures); expect( log, [ @@ -248,8 +246,8 @@ void main() { }); test('signOut/disconnect methods always trigger native calls', () async { - final List> futures = - >[ + final List> futures = + >[ googleSignIn.signOut(), googleSignIn.signOut(), googleSignIn.disconnect(), @@ -273,8 +271,8 @@ void main() { }); test('queue of many concurrent calls', () async { - final List> futures = - >[ + final List> futures = + >[ googleSignIn.signInSilently(), googleSignIn.signOut(), googleSignIn.signIn(), @@ -368,7 +366,7 @@ void main() { await googleSignIn.signIn(); log.clear(); - final GoogleSignInAccount user = googleSignIn.currentUser; + final GoogleSignInAccount user = googleSignIn.currentUser!; final GoogleSignInAuthentication auth = await user.authentication; expect(auth.accessToken, '456'); @@ -415,11 +413,11 @@ void main() { photoUrl: "https://lh5.googleusercontent.com/photo.jpg", ); - GoogleSignIn googleSignIn; + late GoogleSignIn googleSignIn; setUp(() { final MethodChannelGoogleSignIn platformInstance = - GoogleSignInPlatform.instance; + GoogleSignInPlatform.instance as MethodChannelGoogleSignIn; platformInstance.channel.setMockMethodCallHandler( (FakeSignInBackend()..user = kUserData).handleMethodCall); googleSignIn = GoogleSignIn(); @@ -432,7 +430,7 @@ void main() { test('can sign in and sign out', () async { await googleSignIn.signIn(); - final GoogleSignInAccount user = googleSignIn.currentUser; + final GoogleSignInAccount user = googleSignIn.currentUser!; expect(user.displayName, equals(kUserData.displayName)); expect(user.email, equals(kUserData.email)); diff --git a/packages/google_sign_in/google_sign_in/test_driver/integration_test.dart b/packages/google_sign_in/google_sign_in/test_driver/integration_test.dart index f07dba382187..4a27ac2f986b 100644 --- a/packages/google_sign_in/google_sign_in/test_driver/integration_test.dart +++ b/packages/google_sign_in/google_sign_in/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/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md index aa8ad2cff80f..dd6c22fbef29 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,11 @@ +## 2.0.0 + +* Migrate to null-safety. + +## 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/lib/google_sign_in_platform_interface.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart index 966e93551086..3499558f9114 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. import 'dart:async'; -import 'package:meta/meta.dart' show required, visibleForTesting; +import 'package:meta/meta.dart' show visibleForTesting; import 'src/method_channel_google_sign_in.dart'; import 'src/types.dart'; @@ -78,27 +78,28 @@ abstract class GoogleSignInPlatform { /// /// See: /// https://developers.google.com/identity/sign-in/web/reference#gapiauth2initparams - Future init( - {@required String hostedDomain, - List scopes, - SignInOption signInOption, - String clientId}) async { + Future init({ + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String? hostedDomain, + String? clientId, + }) async { throw UnimplementedError('init() has not been implemented.'); } /// Attempts to reuse pre-existing credentials to sign in again, without user interaction. - Future signInSilently() async { + Future signInSilently() async { throw UnimplementedError('signInSilently() has not been implemented.'); } /// Signs in the user with the options specified to [init]. - Future signIn() async { + Future signIn() async { throw UnimplementedError('signIn() has not been implemented.'); } /// Returns the Tokens used to authenticate other API calls. Future getTokens( - {@required String email, bool shouldRecoverAuth}) async { + {required String email, bool? shouldRecoverAuth}) async { throw UnimplementedError('getTokens() has not been implemented.'); } @@ -118,7 +119,7 @@ abstract class GoogleSignInPlatform { } /// Clears any cached information that the plugin may be holding on to. - Future clearAuthCache({@required String token}) async { + Future clearAuthCache({required String token}) async { throw UnimplementedError('clearAuthCache() has not been implemented.'); } diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart index 4d2a34fe0fe7..b36676e2376d 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart @@ -5,7 +5,7 @@ import 'dart:async'; import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show required, visibleForTesting; +import 'package:meta/meta.dart' show visibleForTesting; import '../google_sign_in_platform_interface.dart'; import 'types.dart'; @@ -20,11 +20,12 @@ class MethodChannelGoogleSignIn extends GoogleSignInPlatform { const MethodChannel('plugins.flutter.io/google_sign_in'); @override - Future init( - {@required String hostedDomain, - List scopes = const [], - SignInOption signInOption = SignInOption.standard, - String clientId}) { + Future init({ + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String? hostedDomain, + String? clientId, + }) { return channel.invokeMethod('init', { 'signInOption': signInOption.toString(), 'scopes': scopes, @@ -33,14 +34,14 @@ class MethodChannelGoogleSignIn extends GoogleSignInPlatform { } @override - Future signInSilently() { + Future signInSilently() { return channel .invokeMapMethod('signInSilently') .then(getUserDataFromMap); } @override - Future signIn() { + Future signIn() { return channel .invokeMapMethod('signIn') .then(getUserDataFromMap); @@ -48,12 +49,12 @@ class MethodChannelGoogleSignIn extends GoogleSignInPlatform { @override Future getTokens( - {String email, bool shouldRecoverAuth = true}) { + {required String email, bool? shouldRecoverAuth = true}) { return channel .invokeMapMethod('getTokens', { 'email': email, 'shouldRecoverAuth': shouldRecoverAuth, - }).then(getTokenDataFromMap); + }).then((result) => getTokenDataFromMap(result!)); } @override @@ -67,23 +68,23 @@ class MethodChannelGoogleSignIn extends GoogleSignInPlatform { } @override - Future isSignedIn() { - return channel.invokeMethod('isSignedIn'); + Future isSignedIn() async { + return (await channel.invokeMethod('isSignedIn'))!; } @override - Future clearAuthCache({String token}) { + Future clearAuthCache({String? token}) { return channel.invokeMethod( 'clearAuthCache', - {'token': token}, + {'token': token}, ); } @override - Future requestScopes(List scopes) { - return channel.invokeMethod( + Future requestScopes(List scopes) async { + return (await channel.invokeMethod( 'requestScopes', >{'scopes': scopes}, - ); + ))!; } } diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart index c60402200bdd..a4c5906723dd 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart @@ -24,15 +24,19 @@ enum SignInOption { /// Holds information about the signed in user. class GoogleSignInUserData { - /// Uses the given data to construct an instance. Any of these parameters - /// could be null. - GoogleSignInUserData( - {this.displayName, this.email, this.id, this.photoUrl, this.idToken}); + /// Uses the given data to construct an instance. + GoogleSignInUserData({ + required this.email, + required this.id, + this.displayName, + this.photoUrl, + this.idToken, + }); /// The display name of the signed in user. /// /// Not guaranteed to be present for all users, even when configured. - String displayName; + String? displayName; /// The email address of the signed in user. /// @@ -56,15 +60,15 @@ class GoogleSignInUserData { /// The photo url of the signed in user if the user has a profile picture. /// /// Not guaranteed to be present for all users, even when configured. - String photoUrl; + String? photoUrl; /// A token that can be sent to your own server to verify the authentication /// data. - String idToken; + String? idToken; @override int get hashCode => - hashObjects([displayName, email, id, photoUrl, idToken]); + hashObjects([displayName, email, id, photoUrl, idToken]); @override bool operator ==(dynamic other) { @@ -81,7 +85,7 @@ class GoogleSignInUserData { /// Holds authentication data after sign in. class GoogleSignInTokenData { - /// Either or both parameters may be null. + /// Build `GoogleSignInTokenData`. GoogleSignInTokenData({ this.idToken, this.accessToken, @@ -89,13 +93,13 @@ class GoogleSignInTokenData { }); /// An OpenID Connect ID token for the authenticated user. - String idToken; + String? idToken; /// The OAuth2 access token used to access Google services. - String accessToken; + String? accessToken; /// Server auth code used to access Google Login - String serverAuthCode; + String? serverAuthCode; @override int get hashCode => hash3(idToken, accessToken, serverAuthCode); diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart index 1ae828604af6..f6236d4ca12e 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart @@ -5,23 +5,20 @@ import '../google_sign_in_platform_interface.dart'; /// Converts user data coming from native code into the proper platform interface type. -GoogleSignInUserData getUserDataFromMap(Map data) { +GoogleSignInUserData? getUserDataFromMap(Map? data) { if (data == null) { return null; } return GoogleSignInUserData( + email: data['email']!, + id: data['id']!, displayName: data['displayName'], - email: data['email'], - id: data['id'], photoUrl: data['photoUrl'], idToken: data['idToken']); } /// Converts token data coming from native code into the proper platform interface type. GoogleSignInTokenData getTokenDataFromMap(Map data) { - if (data == null) { - return null; - } return GoogleSignInTokenData( idToken: data['idToken'], accessToken: data['accessToken'], 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..56b4033dcb88 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,20 +3,20 @@ 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: 2.0.0 dependencies: flutter: sdk: flutter - meta: ^1.0.5 - quiver: ">=2.0.0 <3.0.0" + meta: ^1.3.0 + quiver: ^3.0.0 dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 + mockito: ^5.0.0-nullsafety.1 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart index f411b8992821..e2565d799e1f 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart @@ -15,7 +15,7 @@ void main() { test('Cannot be implemented with `implements`', () { expect(() { GoogleSignInPlatform.instance = ImplementsGoogleSignInPlatform(); - }, throwsAssertionError); + }, throwsA(isA())); }); test('Can be extended', () { @@ -23,14 +23,16 @@ void main() { }); test('Can be mocked with `implements`', () { - final ImplementsGoogleSignInPlatform mock = - ImplementsGoogleSignInPlatform(); - when(mock.isMock).thenReturn(true); - GoogleSignInPlatform.instance = mock; + GoogleSignInPlatform.instance = ImplementsWithIsMock(); }); }); } +class ImplementsWithIsMock extends Mock implements GoogleSignInPlatform { + @override + bool get isMock => true; +} + class ImplementsGoogleSignInPlatform extends Mock implements GoogleSignInPlatform {} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart index 5ac34ade1b8d..325f0c10a6ab 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart @@ -29,10 +29,12 @@ const Map kDefaultResponses = { 'disconnect': null, 'isSignedIn': true, 'getTokens': kTokenData, + 'requestScopes': true, }; -final GoogleSignInUserData kUser = getUserDataFromMap(kUserData); -final GoogleSignInTokenData kToken = getTokenDataFromMap(kTokenData); +final GoogleSignInUserData? kUser = getUserDataFromMap(kUserData); +final GoogleSignInTokenData? kToken = + getTokenDataFromMap(kTokenData as Map); void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -42,7 +44,8 @@ void main() { final MethodChannel channel = googleSignIn.channel; final List log = []; - Map responses; // Some tests mutate some kDefaultResponses + late Map + responses; // Some tests mutate some kDefaultResponses setUp(() { responses = Map.from(kDefaultResponses); 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..a5c9e9d2f2bb 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,11 @@ +## 0.10.0 + +* Migrate to null-safety. + +## 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/example/README.md b/packages/google_sign_in/google_sign_in_web/example/README.md new file mode 100644 index 000000000000..0ec01e025570 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/README.md @@ -0,0 +1,21 @@ +# Testing + +This package utilizes the `integration_test` package to run its tests in a web browser. + +See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. + +## Running the tests + +Make sure you have updated to the latest Flutter master. + +1. Check what version of Chrome is running on the machine you're running tests on. + +2. Download and install driver for that version from here: + * + +3. Start the driver using `chromedriver --port=4444` + +4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_driver.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): + + * Single: `./run_test.sh integration_test/TEST_NAME.dart` + * All: `./run_test.sh` diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/auth2_integration.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart similarity index 91% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/auth2_integration.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart index e2f16f2aee43..b80080935d42 100644 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/auth2_integration.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart @@ -3,11 +3,12 @@ // found in the LICENSE file. import 'package:flutter/services.dart'; -import 'package:integration_test/integration_test.dart'; - import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:google_sign_in_web/google_sign_in_web.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:js/js_util.dart' as js_util; + import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; import 'src/test_utils.dart'; @@ -25,7 +26,7 @@ void main() { idToken: expectedTokenData.idToken, ); - GoogleSignInPlugin plugin; + late GoogleSignInPlugin plugin; group('plugin.init() throws a catchable exception', () { setUp(() { @@ -54,7 +55,8 @@ void main() { ); fail('plugin.init should have thrown an exception!'); } catch (e) { - expect(e.code, 'idpiframe_initialization_failed'); + final String code = js_util.getProperty(e, 'code') as String; + expect(code, 'idpiframe_initialization_failed'); } }); }); @@ -62,7 +64,7 @@ void main() { group('other methods also throw catchable exceptions on init fail', () { // This function ensures that init gets called, but for some reason, we // ignored that it has thrown stuff... - void _discardInit() async { + Future _discardInit() async { try { await plugin.init( hostedDomain: 'foo', @@ -135,13 +137,13 @@ void main() { }); testWidgets('signInSilently', (WidgetTester tester) async { - GoogleSignInUserData actualUser = await plugin.signInSilently(); + GoogleSignInUserData actualUser = (await plugin.signInSilently())!; expect(actualUser, expectedUserData); }); testWidgets('signIn', (WidgetTester tester) async { - GoogleSignInUserData actualUser = await plugin.signIn(); + GoogleSignInUserData actualUser = (await plugin.signIn())!; expect(actualUser, expectedUserData); }); @@ -185,7 +187,8 @@ void main() { await plugin.signIn(); fail('plugin.signIn() should have thrown an exception!'); } catch (e) { - expect(e.code, 'popup_closed_by_user'); + final String code = js_util.getProperty(e, 'code') as String; + expect(code, 'popup_closed_by_user'); } }); }); diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_load_integration.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart similarity index 92% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_load_integration.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart index 540369cae370..e0729bcf9b5e 100644 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_load_integration.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart @@ -4,18 +4,19 @@ import 'dart:html' as html; -import 'package:integration_test/integration_test.dart'; - import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:google_sign_in_web/google_sign_in_web.dart'; +import 'package:integration_test/integration_test.dart'; + import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; import 'src/test_utils.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess(GoogleSignInUserData())); + gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess( + GoogleSignInUserData(email: 'test@test.com', id: '1234'))); testWidgets('Plugin is initialized after GAPI fully loads and init is called', (WidgetTester tester) async { diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/gapi_mocks.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart similarity index 100% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/gapi_mocks.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/auth2_init.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart similarity index 100% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/auth2_init.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/gapi.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart similarity index 100% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/gapi.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/google_user.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart similarity index 100% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/google_user.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/test_iife.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart similarity index 100% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/test_iife.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_utils_integration.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart similarity index 54% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_utils_integration.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart index 55b942842b33..e03974a145b7 100644 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_utils_integration.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart @@ -1,27 +1,21 @@ // 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_test/flutter_test.dart'; - -import 'package:integration_test/integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in_web/src/generated/gapiauth2.dart' as gapi; import 'package:google_sign_in_web/src/utils.dart'; -import 'package:mockito/mockito.dart'; - -class MockGoogleUser extends Mock implements gapi.GoogleUser {} - -class MockBasicProfile extends Mock implements gapi.BasicProfile {} +import 'package:integration_test/integration_test.dart'; void main() { // The non-null use cases are covered by the auth2_test.dart file. IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('gapiUserToPluginUserData', () { - var mockUser; + late FakeGoogleUser fakeUser; setUp(() { - mockUser = MockGoogleUser(); + fakeUser = FakeGoogleUser(); }); testWidgets('null user -> null response', (WidgetTester tester) async { @@ -30,21 +24,45 @@ void main() { testWidgets('not signed-in user -> null response', (WidgetTester tester) async { - when(mockUser.isSignedIn()).thenReturn(false); - expect(gapiUserToPluginUserData(mockUser), isNull); + expect(gapiUserToPluginUserData(fakeUser), isNull); }); testWidgets('signed-in, but null profile user -> null response', (WidgetTester tester) async { - when(mockUser.isSignedIn()).thenReturn(true); - expect(gapiUserToPluginUserData(mockUser), isNull); + fakeUser.setIsSignedIn(true); + expect(gapiUserToPluginUserData(fakeUser), isNull); }); testWidgets('signed-in, null userId in profile user -> null response', (WidgetTester tester) async { - when(mockUser.isSignedIn()).thenReturn(true); - when(mockUser.getBasicProfile()).thenReturn(MockBasicProfile()); - expect(gapiUserToPluginUserData(mockUser), isNull); + fakeUser.setIsSignedIn(true); + fakeUser.setBasicProfile(FakeBasicProfile()); + expect(gapiUserToPluginUserData(fakeUser), isNull); }); }); } + +class FakeGoogleUser extends Fake implements gapi.GoogleUser { + bool _isSignedIn = false; + gapi.BasicProfile? _basicProfile; + + @override + bool isSignedIn() => _isSignedIn; + @override + gapi.BasicProfile? getBasicProfile() => _basicProfile; + + void setIsSignedIn(bool isSignedIn) { + _isSignedIn = isSignedIn; + } + + void setBasicProfile(gapi.BasicProfile basicProfile) { + _basicProfile = basicProfile; + } +} + +class FakeBasicProfile extends Fake implements gapi.BasicProfile { + String? _id; + + @override + String? getId() => _id; +} diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/src/test_utils.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart similarity index 100% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/src/test_utils.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart diff --git a/packages/google_sign_in/google_sign_in_web/test/lib/main.dart b/packages/google_sign_in/google_sign_in_web/example/lib/main.dart similarity index 100% rename from packages/google_sign_in/google_sign_in_web/test/lib/main.dart rename to packages/google_sign_in/google_sign_in_web/example/lib/main.dart diff --git a/packages/google_sign_in/google_sign_in_web/test/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml similarity index 57% rename from packages/google_sign_in/google_sign_in_web/test/pubspec.yaml rename to packages/google_sign_in/google_sign_in_web/example/pubspec.yaml index dd0354e81498..385b2ea0861e 100644 --- a/packages/google_sign_in/google_sign_in_web/test/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml @@ -1,23 +1,23 @@ -name: regular_integration_tests +name: google_sign_in_web_integration_tests publish_to: none environment: - sdk: ">=2.2.2 <3.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.27.0-0" # For integration_test from sdk dependencies: flutter: sdk: flutter dev_dependencies: - google_sign_in: ^4.5.3 + http: ^0.13.0 + js: ^0.6.3 flutter_driver: sdk: flutter flutter_test: sdk: flutter - http: ^0.12.2 - mockito: ^4.1.1 integration_test: - path: ../../../integration_test + sdk: flutter dependency_overrides: google_sign_in_web: diff --git a/packages/url_launcher/url_launcher_web/test/run_test b/packages/google_sign_in/google_sign_in_web/example/run_test.sh similarity index 57% rename from packages/url_launcher/url_launcher_web/test/run_test rename to packages/google_sign_in/google_sign_in_web/example/run_test.sh index 74a8526a0fa3..0f76f4a47e16 100755 --- a/packages/url_launcher/url_launcher_web/test/run_test +++ b/packages/google_sign_in/google_sign_in_web/example/run_test.sh @@ -1,17 +1,20 @@ #!/usr/bin/bash + if pgrep -lf chromedriver > /dev/null; then echo "chromedriver is running." if [ $# -eq 0 ]; then echo "No target specified, running all tests..." - find test_driver/ -iname *_integration.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --target='{}' + find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_driver.dart --target='{}' else echo "Running test target: $1..." set -x - flutter drive -d web-server --web-port=7357 --browser-name=chrome --target=$1 + flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_driver.dart --target=$1 fi else echo "chromedriver is not running." fi + + diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/auth2_integration_test.dart b/packages/google_sign_in/google_sign_in_web/example/test_driver/integration_driver.dart similarity index 100% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/auth2_integration_test.dart rename to packages/google_sign_in/google_sign_in_web/example/test_driver/integration_driver.dart diff --git a/packages/google_sign_in/google_sign_in_web/test/web/index.html b/packages/google_sign_in/google_sign_in_web/example/web/index.html similarity index 100% rename from packages/google_sign_in/google_sign_in_web/test/web/index.html rename to packages/google_sign_in/google_sign_in_web/example/web/index.html diff --git a/packages/google_sign_in/google_sign_in_web/ios/google_sign_in_web.podspec b/packages/google_sign_in/google_sign_in_web/ios/google_sign_in_web.podspec deleted file mode 100644 index 5e192172eb4b..000000000000 --- a/packages/google_sign_in/google_sign_in_web/ios/google_sign_in_web.podspec +++ /dev/null @@ -1,21 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'google_sign_in_web' - s.version = '0.8.1' - s.summary = 'No-op implementation of google_sign_in_web web plugin to avoid build issues on iOS' - s.description = <<-DESC - temp fake google_sign_in_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' - end - \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart index dd82852fa350..41e8106802de 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart @@ -6,8 +6,8 @@ import 'dart:async'; import 'dart:html' as html; import 'package:flutter/services.dart'; -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:js/js.dart'; import 'package:meta/meta.dart'; @@ -37,8 +37,8 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { _isGapiInitialized = gapi.inject(gapiUrl).then((_) => gapi.init()); } - Future _isGapiInitialized; - Future _isAuthInitialized; + late Future _isGapiInitialized; + late Future _isAuthInitialized; bool _isInitCalled = false; // This method throws if init hasn't been called at some point in the past. @@ -58,7 +58,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { return Future.wait([_isGapiInitialized, _isAuthInitialized]); } - String _autoDetectedClientId; + String? _autoDetectedClientId; /// Factory method that initializes the plugin with [GoogleSignInPlatform]. static void registerWith(Registrar registrar) { @@ -66,12 +66,13 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { } @override - Future init( - {@required String hostedDomain, - List scopes = const [], - SignInOption signInOption = SignInOption.standard, - String clientId}) async { - final String appClientId = clientId ?? _autoDetectedClientId; + Future init({ + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String? hostedDomain, + String? clientId, + }) async { + final String? appClientId = clientId ?? _autoDetectedClientId; assert( appClientId != null, 'ClientID not set. Either set it on a ' @@ -90,7 +91,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { hosted_domain: hostedDomain, // The js lib wants a space-separated list of values scope: scopes.join(' '), - client_id: appClientId, + client_id: appClientId!, )); Completer isAuthInitialized = Completer(); @@ -119,18 +120,18 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { } @override - Future signInSilently() async { + Future signInSilently() async { await initialized; return gapiUserToPluginUserData( - await auth2.getAuthInstance().currentUser.get()); + await auth2.getAuthInstance()?.currentUser?.get()); } @override - Future signIn() async { + Future signIn() async { await initialized; try { - return gapiUserToPluginUserData(await auth2.getAuthInstance().signIn()); + return gapiUserToPluginUserData(await auth2.getAuthInstance()?.signIn()); } on auth2.GoogleAuthSignInError catch (reason) { throw PlatformException( code: reason.error, @@ -143,30 +144,33 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { @override Future getTokens( - {@required String email, bool shouldRecoverAuth}) async { + {required String email, bool? shouldRecoverAuth}) async { await initialized; - final auth2.GoogleUser currentUser = + final auth2.GoogleUser? currentUser = auth2.getAuthInstance()?.currentUser?.get(); - final auth2.AuthResponse response = currentUser.getAuthResponse(); + final auth2.AuthResponse? response = currentUser?.getAuthResponse(); return GoogleSignInTokenData( - idToken: response.id_token, accessToken: response.access_token); + idToken: response?.id_token, accessToken: response?.access_token); } @override Future signOut() async { await initialized; - return auth2.getAuthInstance().signOut(); + return auth2.getAuthInstance()?.signOut(); } @override Future disconnect() async { await initialized; - final auth2.GoogleUser currentUser = + final auth2.GoogleUser? currentUser = auth2.getAuthInstance()?.currentUser?.get(); + + if (currentUser == null) return; + return currentUser.disconnect(); } @@ -174,16 +178,19 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { Future isSignedIn() async { await initialized; - final auth2.GoogleUser currentUser = + final auth2.GoogleUser? currentUser = auth2.getAuthInstance()?.currentUser?.get(); + + if (currentUser == null) return false; + return currentUser.isSignedIn(); } @override - Future clearAuthCache({String token}) async { + Future clearAuthCache({required String token}) async { await initialized; - return auth2.getAuthInstance().disconnect(); + return auth2.getAuthInstance()?.disconnect(); } @override @@ -194,14 +201,15 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { if (currentUser == null) return false; - final grantedScopes = currentUser.getGrantedScopes(); + final grantedScopes = currentUser.getGrantedScopes() ?? ''; final missingScopes = scopes.where((scope) => !grantedScopes.contains(scope)); if (missingScopes.isEmpty) return true; - return currentUser - .grant(auth2.SigninOptions(scope: missingScopes.join(" "))) ?? - false; + final response = await currentUser + .grant(auth2.SigninOptions(scope: missingScopes.join(' '))); + + return response != null; } } diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart index 97ae9b48dc1b..f0f886ce7880 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart @@ -2,446 +2,53 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@JS() -library gapi; - -import "package:js/js.dart"; -import "package:js/js_util.dart" show promiseToFuture; - /// Type definitions for Google API Client /// Project: https://github.com/google/google-api-javascript-client /// Definitions by: Frank M , grant /// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped /// TypeScript Version: 2.3 -/// The OAuth 2.0 token object represents the OAuth 2.0 token and any associated data. -@anonymous -@JS() -abstract class GoogleApiOAuth2TokenObject { - /// The OAuth 2.0 token. Only present in successful responses - external String get access_token; - external set access_token(String v); - - /// Details about the error. Only present in error responses - external String get error; - external set error(String v); +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi - /// The duration, in seconds, the token is valid for. Only present in successful responses - external String get expires_in; - external set expires_in(String v); - external GoogleApiOAuth2TokenSessionState get session_state; - external set session_state(GoogleApiOAuth2TokenSessionState v); +// ignore_for_file: public_member_api_docs, unused_element - /// The Google API scopes related to this token - external String get state; - external set state(String v); - external factory GoogleApiOAuth2TokenObject( - {String access_token, - String error, - String expires_in, - GoogleApiOAuth2TokenSessionState session_state, - String state}); -} - -@anonymous @JS() -abstract class GoogleApiOAuth2TokenSessionState { - external dynamic /*{ - authuser: string, - }*/ - get extraQueryParams; - external set extraQueryParams( - dynamic - /*{ - authuser: string, - }*/ - v); - external factory GoogleApiOAuth2TokenSessionState( - {dynamic - /*{ - authuser: string, - }*/ - extraQueryParams}); -} +library gapi; -/// Fix for #8215 -/// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/8215 -/// Usage example: -/// https://developers.google.com/identity/sign-in/web/session-state +import 'package:js/js.dart'; // Module gapi typedef void LoadCallback( - [dynamic args1, - dynamic args2, - dynamic args3, - dynamic args4, - dynamic args5]); + [dynamic? args1, + dynamic? args2, + dynamic? args3, + dynamic? args4, + dynamic? args5]); @anonymous @JS() abstract class LoadConfig { external LoadCallback get callback; external set callback(LoadCallback v); - external Function get onerror; - external set onerror(Function v); - external num get timeout; - external set timeout(num v); - external Function get ontimeout; - external set ontimeout(Function v); + external Function? get onerror; + external set onerror(Function? v); + external num? get timeout; + external set timeout(num? v); + external Function? get ontimeout; + external set ontimeout(Function? v); external factory LoadConfig( {LoadCallback callback, - Function onerror, - num timeout, - Function ontimeout}); + Function? onerror, + num? timeout, + Function? ontimeout}); } /*type CallbackOrConfig = LoadConfig | LoadCallback;*/ /// Pragmatically initialize gapi class member. /// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiloadlibraries-callbackorconfig -@JS("gapi.load") +@JS('gapi.load') external void load( String apiName, dynamic /*LoadConfig|LoadCallback*/ callback); // End module gapi -// Module gapi.auth -/// Initiates the OAuth 2.0 authorization process. The browser displays a popup window prompting the user authenticate and authorize. After the user authorizes, the popup closes and the callback function fires. -@JS("gapi.auth.authorize") -external void authorize( - dynamic - /*{ - /** - * The application's client ID. - */ - client_id?: string; - /** - * If true, then login uses "immediate mode", which means that the token is refreshed behind the scenes, and no UI is shown to the user. - */ - immediate?: boolean; - /** - * The OAuth 2.0 response type property. Default: token - */ - response_type?: string; - /** - * The auth scope or scopes to authorize. Auth scopes for individual APIs can be found in their documentation. - */ - scope?: any; - /** - * The user to sign in as. -1 to toggle a multi-account chooser, 0 to default to the user's current account, and 1 to automatically sign in if the user is signed into Google Plus. - */ - authuser?: number; - }*/ - params, - dynamic callback(GoogleApiOAuth2TokenObject token)); - -/// Initializes the authorization feature. Call this when the client loads to prevent popup blockers from blocking the auth window on gapi.auth.authorize calls. -@JS("gapi.auth.init") -external void init(dynamic callback()); - -/// Retrieves the OAuth 2.0 token for the application. -@JS("gapi.auth.getToken") -external GoogleApiOAuth2TokenObject getToken(); - -/// Sets the OAuth 2.0 token for the application. -@JS("gapi.auth.setToken") -external void setToken(GoogleApiOAuth2TokenObject token); - -/// Initiates the client-side Google+ Sign-In OAuth 2.0 flow. -/// When the method is called, the OAuth 2.0 authorization dialog is displayed to the user and when they accept, the callback function is called. -@JS("gapi.auth.signIn") -external void signIn( - dynamic - /*{ - /** - * Your OAuth 2.0 client ID that you obtained from the Google Developers Console. - */ - clientid?: string; - /** - * Directs the sign-in button to store user and session information in a session cookie and HTML5 session storage on the user's client for the purpose of minimizing HTTP traffic and distinguishing between multiple Google accounts a user might be signed into. - */ - cookiepolicy?: string; - /** - * A function in the global namespace, which is called when the sign-in button is rendered and also called after a sign-in flow completes. - */ - callback?: () => void; - /** - * If true, all previously granted scopes remain granted in each incremental request, for incremental authorization. The default value true is correct for most use cases; use false only if employing delegated auth, where you pass the bearer token to a less-trusted component with lower programmatic authority. - */ - includegrantedscopes?: boolean; - /** - * If your app will write moments, list the full URI of the types of moments that you intend to write. - */ - requestvisibleactions?: any; - /** - * The OAuth 2.0 scopes for the APIs that you would like to use as a space-delimited list. - */ - scope?: any; - /** - * If you have an Android app, you can drive automatic Android downloads from your web sign-in flow. - */ - apppackagename?: string; - }*/ - params); - -/// Signs a user out of your app without logging the user out of Google. This method will only work when the user is signed in with Google+ Sign-In. -@JS("gapi.auth.signOut") -external void signOut(); -// End module gapi.auth - -// Module gapi.client -@anonymous -@JS() -abstract class RequestOptions { - /// The URL to handle the request - external String get path; - external set path(String v); - - /// The HTTP request method to use. Default is GET - external String get method; - external set method(String v); - - /// URL params in key-value pair form - external dynamic get params; - external set params(dynamic v); - - /// Additional HTTP request headers - external dynamic get headers; - external set headers(dynamic v); - - /// The HTTP request body (applies to PUT or POST). - external dynamic get body; - external set body(dynamic v); - - /// If supplied, the request is executed immediately and no gapi.client.HttpRequest object is returned - external dynamic Function() get callback; - external set callback(dynamic Function() v); - external factory RequestOptions( - {String path, - String method, - dynamic params, - dynamic headers, - dynamic body, - dynamic Function() callback}); -} - -@anonymous -@JS() -abstract class _RequestOptions { - @JS("gapi.client.init") - external Promise client_init( - dynamic - /*{ - /** - * The API Key to use. - */ - apiKey?: string; - /** - * An array of discovery doc URLs or discovery doc JSON objects. - */ - discoveryDocs?: string[]; - /** - * The app's client ID, found and created in the Google Developers Console. - */ - clientId?: string; - /** - * The scopes to request, as a space-delimited string. - */ - scope?: string, - - hosted_domain?: string; - }*/ - args); -} - -extension RequestOptionsExtensions on RequestOptions {} - -@anonymous -@JS() -abstract class TokenObject { - /// The access token to use in requests. - external String get access_token; - external set access_token(String v); - external factory TokenObject({String access_token}); -} - -/// Creates a HTTP request for making RESTful requests. -/// An object encapsulating the various arguments for this method. -@JS("gapi.client.request") -external HttpRequest request(RequestOptions args); - -/// Creates an RPC Request directly. The method name and version identify the method to be executed and the RPC params are provided upon RPC creation. -@JS("gapi.client.rpcRequest") -external RpcRequest rpcRequest(String method, - [String version, dynamic rpcParams]); - -/// Sets the API key for the application. -@JS("gapi.client.setApiKey") -external void setApiKey(String apiKey); - -/// Retrieves the OAuth 2.0 token for the application. -@JS("gapi.client.getToken") -external GoogleApiOAuth2TokenObject client_getToken(); - -/// Sets the authentication token to use in requests. -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiclientsettokentokenobject -@JS("gapi.client.setToken") -external void client_setToken(TokenObject /*TokenObject|Null*/ token); - -@anonymous -@JS() -abstract class HttpRequestFulfilled { - external T get result; - external set result(T v); - external String get body; - external set body(String v); - external List get headers; - external set headers(List v); - external num get status; - external set status(num v); - external String get statusText; - external set statusText(String v); - external factory HttpRequestFulfilled( - {T result, - String body, - List headers, - num status, - String statusText}); -} - -@anonymous -@JS() -abstract class _HttpRequestFulfilled { - /*external Promise client_load(String name, String version);*/ - /*external void client_load(String name, String version, dynamic callback(), - [String url]); -*/ - @JS("gapi.client.load") - external dynamic /*Promise|void*/ client_load( - String name, String version, - [dynamic callback(), String url]); -} - -extension HttpRequestFulfilledExtensions on HttpRequestFulfilled {} - -@anonymous -@JS() -abstract class HttpRequestRejected { - external dynamic /*dynamic|bool*/ get result; - external set result(dynamic /*dynamic|bool*/ v); - external String get body; - external set body(String v); - external List get headers; - external set headers(List v); - external num get status; - external set status(num v); - external String get statusText; - external set statusText(String v); - external factory HttpRequestRejected( - {dynamic /*dynamic|bool*/ result, - String body, - List headers, - num status, - String statusText}); -} - -/// HttpRequest supports promises. -/// See Google API Client JavaScript Using Promises https://developers.google.com/api-client-library/javascript/features/promises -@JS("gapi.client.HttpRequestPromise") -class HttpRequestPromise {} - -@JS("gapi.client.HttpRequestPromise") -abstract class _HttpRequestPromise { - /// Taken and adapted from https://github.com/Microsoft/TypeScript/blob/v2.3.1/lib/lib.es5.d.ts#L1343 - external Promise then/**/( - [dynamic /*TResult1|PromiseLike Function(HttpRequestFulfilled)|dynamic|Null*/ onfulfilled, - dynamic /*TResult2|PromiseLike Function(HttpRequestRejected)|dynamic|Null*/ onrejected, - dynamic opt_context]); -} - -extension HttpRequestPromiseExtensions on HttpRequestPromise { - Future then( - [dynamic /*TResult1|PromiseLike Function(HttpRequestFulfilled)|dynamic|Null*/ onfulfilled, - dynamic /*TResult2|PromiseLike Function(HttpRequestRejected)|dynamic|Null*/ onrejected, - dynamic opt_context]) { - final Object t = this; - final _HttpRequestPromise tt = t; - return promiseToFuture(tt.then(onfulfilled, onrejected, opt_context)); - } -} - -/// An object encapsulating an HTTP request. This object is not instantiated directly, rather it is returned by gapi.client.request. -@JS("gapi.client.HttpRequest") -class HttpRequest extends HttpRequestPromise { - /// Executes the request and runs the supplied callback on response. - external void execute( - dynamic callback( - - /// contains the response parsed as JSON. If the response is not JSON, this field will be false. - T jsonResp, - - /// is the HTTP response. It is JSON, and can be parsed to an object - dynamic - /*{ - body: string; - headers: any[]; - status: number; - statusText: string; - }*/ - rawResp)); -} - -/// Represents an HTTP Batch operation. Individual HTTP requests are added with the add method and the batch is executed using execute. -@JS("gapi.client.HttpBatch") -class HttpBatch { - /// Adds a gapi.client.HttpRequest to the batch. - external void add(HttpRequest httpRequest, - [dynamic - /*{ - /** - * Identifies the response for this request in the map of batch responses. If one is not provided, the system generates a random ID. - */ - id: string; - callback: ( - /** - * is the response for this request only. Its format is defined by the API method being called. - */ - individualResponse: any, - /** - * is the raw batch ID-response map as a string. It contains all responses to all requests in the batch. - */ - rawBatchResponse: any - ) => any - }*/ - opt_params]); - - /// Executes all requests in the batch. The supplied callback is executed on success or failure. - external void execute( - dynamic callback( - - /// is an ID-response map of each requests response. - dynamic responseMap, - - /// is the same response, but as an unparsed JSON-string. - String rawBatchResponse)); -} - -/// Similar to gapi.client.HttpRequest except this object encapsulates requests generated by registered methods. -@JS("gapi.client.RpcRequest") -class RpcRequest { - /// Executes the request and runs the supplied callback with the response. - external void callback( - void callback( - - /// contains the response parsed as JSON. If the response is not JSON, this field will be false. - dynamic jsonResp, - - /// is the same as jsonResp, except it is a raw string that has not been parsed. It is typically used when the response is not JSON. - String rawResp)); -} - -// End module gapi.client -@JS() -abstract class Promise { - external factory Promise( - void executor(void resolve(T result), Function reject)); - external Promise then(void onFulfilled(T result), [Function onRejected]); -} +// Manually removed gapi.auth and gapi.client, unused by this plugin. diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart index ed7a2816d55e..b2b5c368b6ab 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart @@ -2,12 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@JS() -library gapiauth2; - -import "package:js/js.dart"; -import "package:js/js_util.dart" show promiseToFuture; - /// Type definitions for non-npm package Google Sign-In API 0.0 /// Project: https://developers.google.com/identity/sign-in/web/ /// Definitions by: Derek Lawless @@ -16,14 +10,24 @@ import "package:js/js_util.dart" show promiseToFuture; /// +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi.auth2 + +// ignore_for_file: public_member_api_docs, unused_element + +@JS() +library gapiauth2; + +import 'package:js/js.dart'; +import 'package:js/js_util.dart' show promiseToFuture; + @anonymous @JS() class GoogleAuthInitFailureError { external String get error; - external set error(String value); + external set error(String? value); external String get details; - external set details(String value); + external set details(String? value); } @anonymous @@ -33,16 +37,23 @@ class GoogleAuthSignInError { external set error(String value); } +@anonymous +@JS() +class OfflineAccessResponse { + external String? get code; + external set code(String? value); +} + // Module gapi.auth2 /// GoogleAuth is a singleton class that provides methods to allow the user to sign in with a Google account, /// get the user's current sign-in status, get specific data from the user's Google profile, /// request additional scopes, and sign out from the current account. -@JS("gapi.auth2.GoogleAuth") +@JS('gapi.auth2.GoogleAuth') class GoogleAuth { external IsSignedIn get isSignedIn; external set isSignedIn(IsSignedIn v); - external CurrentUser get currentUser; - external set currentUser(CurrentUser v); + external CurrentUser? get currentUser; + external set currentUser(CurrentUser? v); /// Calls the onInit function when the GoogleAuth object is fully initialized, or calls the onFailure function if /// initialization fails. @@ -57,7 +68,7 @@ class GoogleAuth { /// Attaches the sign-in flow to the specified container's click handler. external dynamic attachClickHandler( - dynamic container, + dynamic? container, SigninOptions options, dynamic onsuccess(GoogleUser googleUser), dynamic onfailure(String reason)); @@ -68,22 +79,20 @@ class GoogleAuth { abstract class _GoogleAuth { external Promise signIn( [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); - external Promise grantOfflineAccess( - [OfflineAccessOptions options]); + external Promise grantOfflineAccess( + [OfflineAccessOptions? options]); } extension GoogleAuthExtensions on GoogleAuth { Future signIn( [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]) { - final Object t = this; - final _GoogleAuth tt = t; + final _GoogleAuth tt = this as _GoogleAuth; return promiseToFuture(tt.signIn(options)); } - Future grantOfflineAccess( - [OfflineAccessOptions options]) { - final Object t = this; - final _GoogleAuth tt = t; + Future grantOfflineAccess( + [OfflineAccessOptions? options]) { + final _GoogleAuth tt = this as _GoogleAuth; return promiseToFuture(tt.grantOfflineAccess(options)); } } @@ -116,42 +125,52 @@ abstract class SigninOptions { /// The package name of the Android app to install over the air. /// See Android app installs from your web site: /// https://developers.google.com/identity/sign-in/web/android-app-installs - external String get app_package_name; - external set app_package_name(String v); + external String? get app_package_name; + external set app_package_name(String? v); /// Fetch users' basic profile information when they sign in. /// Adds 'profile', 'email' and 'openid' to the requested scopes. /// True if unspecified. - external bool get fetch_basic_profile; - external set fetch_basic_profile(bool v); + external bool? get fetch_basic_profile; + external set fetch_basic_profile(bool? v); /// Specifies whether to prompt the user for re-authentication. /// See OpenID Connect Request Parameters: /// https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters - external String get prompt; - external set prompt(String v); + external String? get prompt; + external set prompt(String? v); /// The scopes to request, as a space-delimited string. /// Optional if fetch_basic_profile is not set to false. - external String get scope; - external set scope(String v); + external String? get scope; + external set scope(String? v); /// The UX mode to use for the sign-in flow. /// By default, it will open the consent flow in a popup. - external String /*'popup'|'redirect'*/ get ux_mode; - external set ux_mode(String /*'popup'|'redirect'*/ v); + external String? /*'popup'|'redirect'*/ get ux_mode; + external set ux_mode(String? /*'popup'|'redirect'*/ v); /// If using ux_mode='redirect', this parameter allows you to override the default redirect_uri that will be used at the end of the consent flow. /// The default redirect_uri is the current URL stripped of query parameters and hash fragment. - external String get redirect_uri; - external set redirect_uri(String v); + external String? get redirect_uri; + external set redirect_uri(String? v); + + // When your app knows which user it is trying to authenticate, it can provide this parameter as a hint to the authentication server. + // Passing this hint suppresses the account chooser and either pre-fill the email box on the sign-in form, or select the proper session (if the user is using multiple sign-in), + // which can help you avoid problems that occur if your app logs in the wrong user account. The value can be either an email address or the sub string, + // which is equivalent to the user's Google ID. + // https://developers.google.com/identity/protocols/OpenIDConnect?hl=en#authenticationuriparameters + external String? get login_hint; + external set login_hint(String? v); + external factory SigninOptions( {String app_package_name, bool fetch_basic_profile, String prompt, String scope, String /*'popup'|'redirect'*/ ux_mode, - String redirect_uri}); + String redirect_uri, + String login_hint}); } /// Definitions by: John @@ -160,12 +179,12 @@ abstract class SigninOptions { @anonymous @JS() abstract class OfflineAccessOptions { - external String get scope; - external set scope(String v); - external String /*'select_account'|'consent'*/ get prompt; - external set prompt(String /*'select_account'|'consent'*/ v); - external String get app_package_name; - external set app_package_name(String v); + external String? get scope; + external set scope(String? v); + external String? /*'select_account'|'consent'*/ get prompt; + external set prompt(String? /*'select_account'|'consent'*/ v); + external String? get app_package_name; + external set app_package_name(String? v); external factory OfflineAccessOptions( {String scope, String /*'select_account'|'consent'*/ prompt, @@ -178,98 +197,99 @@ abstract class OfflineAccessOptions { @JS() abstract class ClientConfig { /// The app's client ID, found and created in the Google Developers Console. - external String get client_id; - external set client_id(String v); + external String? get client_id; + external set client_id(String? v); /// The domains for which to create sign-in cookies. Either a URI, single_host_origin, or none. /// Defaults to single_host_origin if unspecified. - external String get cookie_policy; - external set cookie_policy(String v); + external String? get cookie_policy; + external set cookie_policy(String? v); /// The scopes to request, as a space-delimited string. Optional if fetch_basic_profile is not set to false. - external String get scope; - external set scope(String v); + external String? get scope; + external set scope(String? v); /// Fetch users' basic profile information when they sign in. Adds 'profile' and 'email' to the requested scopes. True if unspecified. - external bool get fetch_basic_profile; - external set fetch_basic_profile(bool v); + external bool? get fetch_basic_profile; + external set fetch_basic_profile(bool? v); /// The Google Apps domain to which users must belong to sign in. This is susceptible to modification by clients, /// so be sure to verify the hosted domain property of the returned user. Use GoogleUser.getHostedDomain() on the client, /// and the hd claim in the ID Token on the server to verify the domain is what you expected. - external String get hosted_domain; - external set hosted_domain(String v); + external String? get hosted_domain; + external set hosted_domain(String? v); /// Used only for OpenID 2.0 client migration. Set to the value of the realm that you are currently using for OpenID 2.0, /// as described in OpenID 2.0 (Migration). - external String get openid_realm; - external set openid_realm(String v); + external String? get openid_realm; + external set openid_realm(String? v); /// The UX mode to use for the sign-in flow. /// By default, it will open the consent flow in a popup. - external String /*'popup'|'redirect'*/ get ux_mode; - external set ux_mode(String /*'popup'|'redirect'*/ v); + external String? /*'popup'|'redirect'*/ get ux_mode; + external set ux_mode(String? /*'popup'|'redirect'*/ v); /// If using ux_mode='redirect', this parameter allows you to override the default redirect_uri that will be used at the end of the consent flow. /// The default redirect_uri is the current URL stripped of query parameters and hash fragment. - external String get redirect_uri; - external set redirect_uri(String v); + external String? get redirect_uri; + external set redirect_uri(String? v); external factory ClientConfig( {String client_id, String cookie_policy, String scope, bool fetch_basic_profile, - String hosted_domain, + String? hosted_domain, String openid_realm, String /*'popup'|'redirect'*/ ux_mode, String redirect_uri}); } -@JS("gapi.auth2.SigninOptionsBuilder") +@JS('gapi.auth2.SigninOptionsBuilder') class SigninOptionsBuilder { external dynamic setAppPackageName(String name); external dynamic setFetchBasicProfile(bool fetch); external dynamic setPrompt(String prompt); external dynamic setScope(String scope); + external dynamic setLoginHint(String hint); } @anonymous @JS() abstract class BasicProfile { - external String getId(); - external String getName(); - external String getGivenName(); - external String getFamilyName(); - external String getImageUrl(); - external String getEmail(); + external String? getId(); + external String? getName(); + external String? getGivenName(); + external String? getFamilyName(); + external String? getImageUrl(); + external String? getEmail(); } /// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authresponse @anonymous @JS() abstract class AuthResponse { - external String get access_token; - external set access_token(String v); - external String get id_token; - external set id_token(String v); - external String get login_hint; - external set login_hint(String v); - external String get scope; - external set scope(String v); - external num get expires_in; - external set expires_in(num v); - external num get first_issued_at; - external set first_issued_at(num v); - external num get expires_at; - external set expires_at(num v); + external String? get access_token; + external set access_token(String? v); + external String? get id_token; + external set id_token(String? v); + external String? get login_hint; + external set login_hint(String? v); + external String? get scope; + external set scope(String? v); + external num? get expires_in; + external set expires_in(num? v); + external num? get first_issued_at; + external set first_issued_at(num? v); + external num? get expires_at; + external set expires_at(num? v); external factory AuthResponse( - {String access_token, - String id_token, - String login_hint, - String scope, - num expires_in, - num first_issued_at, - num expires_at}); + {String? access_token, + String? id_token, + String? login_hint, + String? scope, + num? expires_in, + num? first_issued_at, + num? expires_at}); } /// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeconfig @@ -280,22 +300,22 @@ abstract class AuthorizeConfig { external set client_id(String v); external String get scope; external set scope(String v); - external String get response_type; - external set response_type(String v); - external String get prompt; - external set prompt(String v); - external String get cookie_policy; - external set cookie_policy(String v); - external String get hosted_domain; - external set hosted_domain(String v); - external String get login_hint; - external set login_hint(String v); - external String get app_package_name; - external set app_package_name(String v); - external String get openid_realm; - external set openid_realm(String v); - external bool get include_granted_scopes; - external set include_granted_scopes(bool v); + external String? get response_type; + external set response_type(String? v); + external String? get prompt; + external set prompt(String? v); + external String? get cookie_policy; + external set cookie_policy(String? v); + external String? get hosted_domain; + external set hosted_domain(String? v); + external String? get login_hint; + external set login_hint(String? v); + external String? get app_package_name; + external set app_package_name(String? v); + external String? get openid_realm; + external set openid_realm(String? v); + external bool? get include_granted_scopes; + external set include_granted_scopes(bool? v); external factory AuthorizeConfig( {String client_id, String scope, @@ -348,34 +368,31 @@ abstract class AuthorizeResponse { @JS() abstract class GoogleUser { /// Get the user's unique ID string. - external String getId(); + external String? getId(); /// Returns true if the user is signed in. external bool isSignedIn(); /// Get the user's Google Apps domain if the user signed in with a Google Apps account. - external String getHostedDomain(); + external String? getHostedDomain(); /// Get the scopes that the user granted as a space-delimited string. - external String getGrantedScopes(); + external String? getGrantedScopes(); /// Get the user's basic profile information. - external BasicProfile getBasicProfile(); + external BasicProfile? getBasicProfile(); /// Get the response object from the user's auth session. + // This returns an empty JS object when the user hasn't attempted to sign in. external AuthResponse getAuthResponse([bool includeAuthorizationData]); /// Returns true if the user granted the specified scopes. external bool hasGrantedScopes(String scopes); - /// Signs in the user. Use this method to request additional scopes for incremental - /// authorization or to sign in a user after the user has signed out. - /// When you use GoogleUser.signIn(), the sign-in flow skips the account chooser step. - /// See GoogleAuth.signIn(). - external dynamic signIn( - [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); - - /// See GoogleUser.signIn() + // Has the API for grant and grantOfflineAccess changed? + /// Request additional scopes to the user. + /// + /// See GoogleAuth.signIn() for the list of parameters and the error code. external dynamic grant( [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); @@ -391,35 +408,35 @@ abstract class GoogleUser { @anonymous @JS() abstract class _GoogleUser { + /// Forces a refresh of the access token, and then returns a Promise for the new AuthResponse. external Promise reloadAuthResponse(); } extension GoogleUserExtensions on GoogleUser { Future reloadAuthResponse() { - final Object t = this; - final _GoogleUser tt = t; + final _GoogleUser tt = this as _GoogleUser; return promiseToFuture(tt.reloadAuthResponse()); } } /// Initializes the GoogleAuth object. /// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2initparams -@JS("gapi.auth2.init") +@JS('gapi.auth2.init') external GoogleAuth init(ClientConfig params); /// Returns the GoogleAuth object. You must initialize the GoogleAuth object with gapi.auth2.init() before calling this method. -@JS("gapi.auth2.getAuthInstance") -external GoogleAuth getAuthInstance(); +@JS('gapi.auth2.getAuthInstance') +external GoogleAuth? getAuthInstance(); /// Performs a one time OAuth 2.0 authorization. /// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeparams-callback -@JS("gapi.auth2.authorize") +@JS('gapi.auth2.authorize') external void authorize( AuthorizeConfig params, void callback(AuthorizeResponse response)); // End module gapi.auth2 // Module gapi.signin2 -@JS("gapi.signin2.render") +@JS('gapi.signin2.render') external void render( dynamic id, dynamic diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart index f954ff1dce6b..0d3e4165227c 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart @@ -20,7 +20,7 @@ external set gapiOnloadCallback(Function callback); /// This is only exposed for testing. It shouldn't be accessed by users of the /// plugin as it could break at any point. @visibleForTesting -const String kGapiOnloadCallbackFunctionName = "gapiOnloadCallback"; +const String kGapiOnloadCallbackFunctionName = 'gapiOnloadCallback'; String _addOnloadToScript(String url) => url.startsWith('data:') ? url : '$url?onload=$kGapiOnloadCallbackFunctionName'; diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart index 36bb52dce0f3..98cb24efaeeb 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart @@ -9,13 +9,22 @@ import 'package:google_sign_in_platform_interface/google_sign_in_platform_interf import 'generated/gapiauth2.dart' as auth2; -/// Injects a bunch of libraries in the and returns a -/// Future that resolves when all load. -Future injectJSLibraries(List libraries, - {html.HtmlElement target /*, Duration timeout */}) { +/// Injects a list of JS [libraries] as `script` tags into a [target] [html.HtmlElement]. +/// +/// If [target] is not provided, it defaults to the web app's `head` tag (see `web/index.html`). +/// [libraries] is a list of URLs that are used as the `src` attribute of `script` tags +/// to which an `onLoad` listener is attached (one per URL). +/// +/// Returns a [Future] that resolves when all of the `script` tags `onLoad` events trigger. +Future injectJSLibraries( + List libraries, { + html.HtmlElement? target, +}) { final List> loading = >[]; final List tags = []; + final html.Element targetElement = target ?? html.querySelector('head')!; + libraries.forEach((String library) { final html.ScriptElement script = html.ScriptElement() ..async = true @@ -25,24 +34,26 @@ Future injectJSLibraries(List libraries, loading.add(script.onLoad.first); tags.add(script); }); - (target ?? html.querySelector('head')).children.addAll(tags); + + targetElement.children.addAll(tags); return Future.wait(loading); } -/// Utility method that converts `currentUser` to the equivalent -/// [GoogleSignInUserData]. +/// Utility method that converts `currentUser` to the equivalent [GoogleSignInUserData]. +/// /// This method returns `null` when the [currentUser] is not signed in. -GoogleSignInUserData gapiUserToPluginUserData(auth2.GoogleUser currentUser) { +GoogleSignInUserData? gapiUserToPluginUserData(auth2.GoogleUser? currentUser) { final bool isSignedIn = currentUser?.isSignedIn() ?? false; - final auth2.BasicProfile profile = currentUser?.getBasicProfile(); + final auth2.BasicProfile? profile = currentUser?.getBasicProfile(); if (!isSignedIn || profile?.getId() == null) { return null; } + return GoogleSignInUserData( displayName: profile?.getName(), - email: profile?.getEmail(), - id: profile?.getId(), + email: profile?.getEmail() ?? '', + id: profile?.getId() ?? '', photoUrl: profile?.getImageUrl(), - idToken: currentUser.getAuthResponse()?.id_token, + idToken: currentUser?.getAuthResponse().id_token, ); } 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..ae6807cd9231 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.10.0 flutter: plugin: @@ -12,23 +12,19 @@ flutter: fileName: google_sign_in_web.dart dependencies: - google_sign_in_platform_interface: ^1.1.0 + google_sign_in_platform_interface: ^2.0.0 flutter: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 - js: ^0.6.1 + meta: ^1.3.0 + js: ^0.6.3 dev_dependencies: flutter_test: sdk: flutter - google_sign_in: ^4.0.14 - pedantic: ^1.8.0 - mockito: ^4.1.1 - integration_test: - path: ../../integration_test + pedantic: ^1.10.0 environment: - sdk: ">=2.6.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/google_sign_in/google_sign_in_web/test/README.md b/packages/google_sign_in/google_sign_in_web/test/README.md index 7c48d024ba57..7c5b4ad682ba 100644 --- a/packages/google_sign_in/google_sign_in_web/test/README.md +++ b/packages/google_sign_in/google_sign_in_web/test/README.md @@ -1,17 +1,5 @@ -# Running browser_tests +## test -Make sure you have updated to the latest Flutter master. +This package uses integration tests for testing. -1. Check what version of Chrome is running on the machine you're running tests on. - -2. Download and install driver for that version from here: - * - -3. Start the driver using `chromedriver --port=4444` - -4. Change into the `test` directory of your clone. - -5. Run tests: `flutter drive -d web-server --browser-name=chrome --target=test_driver/TEST_NAME_integration.dart`, or (in Linux): - - * Single: `./run_test test_driver/TEST_NAME_integration.dart` - * All: `./run_test` +See `example/README.md` for more info. diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_utils_integration_test.dart b/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_utils_integration_test.dart deleted file mode 100644 index 39444c0daa24..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_utils_integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// 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 'package:integration_test/integration_test_driver.dart'; - -Future main() async => integrationDriver(); diff --git a/packages/google_sign_in/google_sign_in_web/test/tests_exist_elsewhere_test.dart b/packages/google_sign_in/google_sign_in_web/test/tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..334f52186d9d --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/tests_exist_elsewhere_test.dart @@ -0,0 +1,10 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Tell the user where to find the real tests', () { + print('---'); + print('This package uses integration_test for its tests.'); + print('See `example/README.md` for more info.'); + print('---'); + }); +} diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 7752831865d3..b378b308c467 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,44 @@ +## 0.7.0 + +* Migrate to nullsafety +* Breaking Changes: + * Removed the deprecated methods: `ImagePicker.pickImage`, `ImagePicker.pickVideo`, +`ImagePicker.retrieveLostData` + +## 0.6.7+22 + +* iOS: update XCUITests to separate each test session. + +## 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..e30fabd2d071 100644 --- a/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m +++ b/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m @@ -16,33 +16,57 @@ @interface ImagePickerFromGalleryUITests : XCTestCase @implementation ImagePickerFromGalleryUITests - (void)setUp { + [super setUp]; // Delete the app if already exists, to test permission popups 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, "%@", + weakSelf.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, "%@", + weakSelf.app.debugDescription); + XCTFail(@"Failed due to not able to find ok button " + @"with %@ seconds", + @(kElementWaitingTime)); + } + [ok tap]; } return YES; }]; } +- (void)tearDown { + [super tearDown]; + [self.app terminate]; +} + - (void)testPickingFromGallery { - [self launchPickerAndCancel]; [self launchPickerAndPick]; } +- (void)testCancel { + [self launchPickerAndCancel]; +} + - (void)launchPickerAndCancel { // Find and tap on the pick from gallery button. NSPredicate* predicateToFindImageFromGalleryButton = @@ -92,10 +116,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", @@ -134,6 +169,10 @@ - (void)launchPickerAndPick { XCTAssertTrue(pickButton.exists); [pickButton tap]; + // There is a known bug where the permission popups interruption won't get fired until a tap + // happened in the app. We expect a permission popup so we do a tap here. + [self.app tap]; + // Find an image and tap on it. (IOS 14 UI, images are showing directly) XCUIElement* aImage; if (@available(iOS 14, *)) { @@ -151,6 +190,7 @@ - (void)launchPickerAndPick { identifier:@"PhotosGridView"] .cells.firstMatch; } + os_log_error(OS_LOG_DEFAULT, "description before picking image %@", self.app.debugDescription); if (![aImage waitForExistenceWithTimeout:kElementWaitingTime]) { os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); XCTFail(@"Failed due to not able to find an image with %@ seconds", @(kElementWaitingTime)); diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 3047ac13f235..54e3a1ae4cd2 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -29,60 +29,63 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, this.title}) : super(key: key); - final String title; + final String? title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { - PickedFile _imageFile; + PickedFile? _imageFile; dynamic _pickImageError; bool isVideo = false; - VideoPlayerController _controller; - VideoPlayerController _toBeDisposed; - String _retrieveDataError; + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; final ImagePicker _picker = ImagePicker(); final TextEditingController maxWidthController = TextEditingController(); final TextEditingController maxHeightController = TextEditingController(); final TextEditingController qualityController = TextEditingController(); - Future _playVideo(PickedFile file) async { + Future _playVideo(PickedFile? file) async { if (file != null && mounted) { await _disposeVideoController(); + late VideoPlayerController controller; if (kIsWeb) { - _controller = VideoPlayerController.network(file.path); - // In web, most browsers won't honor a programmatic call to .play - // if the video has a sound track (and is not muted). - // Mute the video so it auto-plays in web! - // This is not needed if the call to .play is the result of user - // interaction (clicking on a "play" button, for example). - await _controller.setVolume(0.0); + controller = VideoPlayerController.network(file.path); } else { - _controller = VideoPlayerController.file(File(file.path)); - await _controller.setVolume(1.0); + controller = VideoPlayerController.file(File(file.path)); } - await _controller.initialize(); - await _controller.setLooping(true); - await _controller.play(); + _controller = controller; + // In web, most browsers won't honor a programmatic call to .play + // if the video has a sound track (and is not muted). + // Mute the video so it auto-plays in web! + // This is not needed if the call to .play is the result of user + // interaction (clicking on a "play" button, for example). + final double volume = kIsWeb ? 0.0 : 1.0; + await controller.setVolume(volume); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); setState(() {}); } } - void _onImageButtonPressed(ImageSource source, {BuildContext context}) async { + void _onImageButtonPressed(ImageSource source, + {BuildContext? context}) async { if (_controller != null) { - await _controller.setVolume(0.0); + await _controller!.setVolume(0.0); } if (isVideo) { - final PickedFile file = await _picker.getVideo( + final PickedFile? file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else { - await _displayPickImageDialog(context, - (double maxWidth, double maxHeight, int quality) async { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { try { final pickedFile = await _picker.getImage( source: source, @@ -105,8 +108,8 @@ class _MyHomePageState extends State { @override void deactivate() { if (_controller != null) { - _controller.setVolume(0.0); - _controller.pause(); + _controller!.setVolume(0.0); + _controller!.pause(); } super.deactivate(); } @@ -122,14 +125,14 @@ class _MyHomePageState extends State { Future _disposeVideoController() async { if (_toBeDisposed != null) { - await _toBeDisposed.dispose(); + await _toBeDisposed!.dispose(); } _toBeDisposed = _controller; _controller = null; } Widget _previewVideo() { - final Text retrieveError = _getRetrieveErrorWidget(); + final Text? retrieveError = _getRetrieveErrorWidget(); if (retrieveError != null) { return retrieveError; } @@ -146,7 +149,7 @@ class _MyHomePageState extends State { } Widget _previewImage() { - final Text retrieveError = _getRetrieveErrorWidget(); + final Text? retrieveError = _getRetrieveErrorWidget(); if (retrieveError != null) { return retrieveError; } @@ -154,10 +157,10 @@ class _MyHomePageState extends State { if (kIsWeb) { // Why network? // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform - return Image.network(_imageFile.path); + return Image.network(_imageFile!.path); } else { return Semantics( - child: Image.file(File(_imageFile.path)), + child: Image.file(File(_imageFile!.path)), label: 'image_picker_example_picked_image'); } } else if (_pickImageError != null) { @@ -189,7 +192,7 @@ class _MyHomePageState extends State { }); } } else { - _retrieveDataError = response.exception.code; + _retrieveDataError = response.exception!.code; } } @@ -197,7 +200,7 @@ class _MyHomePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(widget.title), + title: Text(widget.title!), ), body: Center( child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android @@ -288,9 +291,9 @@ class _MyHomePageState extends State { ); } - Text _getRetrieveErrorWidget() { + Text? _getRetrieveErrorWidget() { if (_retrieveDataError != null) { - final Text result = Text(_retrieveDataError); + final Text result = Text(_retrieveDataError!); _retrieveDataError = null; return result; } @@ -327,22 +330,22 @@ 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 + double? width = maxWidthController.text.isNotEmpty ? double.parse(maxWidthController.text) : null; - double height = maxHeightController.text.isNotEmpty + double? height = maxHeightController.text.isNotEmpty ? double.parse(maxHeightController.text) : null; - int quality = qualityController.text.isNotEmpty + int? quality = qualityController.text.isNotEmpty ? int.parse(qualityController.text) : null; onPick(width, height, quality); @@ -355,27 +358,27 @@ class _MyHomePageState extends State { } typedef void OnPickImageCallback( - double maxWidth, double maxHeight, int quality); + double? maxWidth, double? maxHeight, int? quality); class AspectRatioVideo extends StatefulWidget { AspectRatioVideo(this.controller); - final VideoPlayerController controller; + final VideoPlayerController? controller; @override AspectRatioVideoState createState() => AspectRatioVideoState(); } class AspectRatioVideoState extends State { - VideoPlayerController get controller => widget.controller; + VideoPlayerController? get controller => widget.controller; bool initialized = false; void _onVideoControllerUpdate() { if (!mounted) { return; } - if (initialized != controller.value.initialized) { - initialized = controller.value.initialized; + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; setState(() {}); } } @@ -383,12 +386,12 @@ class AspectRatioVideoState extends State { @override void initState() { super.initState(); - controller.addListener(_onVideoControllerUpdate); + controller!.addListener(_onVideoControllerUpdate); } @override void dispose() { - controller.removeListener(_onVideoControllerUpdate); + controller!.removeListener(_onVideoControllerUpdate); super.dispose(); } @@ -397,8 +400,8 @@ class AspectRatioVideoState extends State { if (initialized) { return Center( child: AspectRatio( - aspectRatio: controller.value?.aspectRatio, - child: VideoPlayer(controller), + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), ), ); } else { diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index 0ff2f280e2ab..ceafc317fa82 100755 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -3,24 +3,28 @@ description: Demonstrates how to use the image_picker plugin. author: Flutter Team dependencies: - video_player: ^0.10.3 + video_player: ^2.0.0-nullsafety.7 flutter: sdk: flutter - flutter_plugin_android_lifecycle: ^1.0.2 + flutter_plugin_android_lifecycle: ^2.0.0 image_picker: + # When depending on this package from a real application you should use: + # image_picker: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - image_picker_for_web: ^0.1.0 dev_dependencies: flutter_driver: sdk: flutter integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.10.0" diff --git a/packages/image_picker/image_picker/integration_test/old_image_picker_test.dart b/packages/image_picker/image_picker/integration_test/old_image_picker_test.dart index d21a4e0cdfa3..76c971c2881b 100644 --- a/packages/image_picker/image_picker/integration_test/old_image_picker_test.dart +++ b/packages/image_picker/image_picker/integration_test/old_image_picker_test.dart @@ -1,3 +1,5 @@ +// @dart=2.9 + import 'package:integration_test/integration_test.dart'; void main() { diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m index def690ca651e..768fdfeacd7e 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..22315100c961 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -5,7 +5,6 @@ // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package import 'dart:async'; -import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -18,7 +17,6 @@ export 'package:image_picker_platform_interface/image_picker_platform_interface. ImageSource, CameraDevice, LostData, - LostDataResponse, PickedFile, RetrieveType; @@ -27,47 +25,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; - - /// Returns a [File] object pointing to the image that was picked. - /// - /// The returned [File] is intended to be used within a single APP session. Do not save the file path and use it across sessions. - /// - /// The `source` argument controls where the image comes from. This can - /// be either [ImageSource.camera] or [ImageSource.gallery]. - /// - /// If specified, the image will be at most `maxWidth` wide and - /// `maxHeight` tall. Otherwise the image will be returned at it's - /// original width and height. - /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 - /// where 100 is the original/max quality. If `imageQuality` is null, the image with - /// the original quality will be returned. Compression is only supportted for certain - /// image types such as JPEG. If compression is not supported for the image that is picked, - /// an warning message will be logged. - /// - /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. - /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. - /// Defaults to [CameraDevice.rear]. - /// - /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost - /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. - @Deprecated('Use imagePicker.getImage() method instead.') - static Future pickImage( - {@required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, - CameraDevice preferredCameraDevice = CameraDevice.rear}) async { - String path = await platform.pickImagePath( - source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: imageQuality, - preferredCameraDevice: preferredCameraDevice, - ); - - return path == null ? null : File(path); - } + static ImagePickerPlatform get platform => ImagePickerPlatform.instance; /// Returns a [PickedFile] object wrapping the image that was picked. /// @@ -96,11 +54,11 @@ class ImagePicker { /// /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost /// in this call. You can then call [getLostData] when your app relaunches to retrieve the lost data. - Future getImage({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { return platform.pickImage( @@ -112,36 +70,6 @@ class ImagePicker { ); } - /// Returns a [File] object pointing to the video that was picked. - /// - /// The returned [File] is intended to be used within a single APP session. Do not save the file path and use it across sessions. - /// - /// The [source] argument controls where the video comes from. This can - /// be either [ImageSource.camera] or [ImageSource.gallery]. - /// - /// The [maxDuration] argument specifies the maximum duration of the captured video. If no [maxDuration] is specified, - /// the maximum duration will be infinite. - /// - /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. - /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. - /// Defaults to [CameraDevice.rear]. - /// - /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost - /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. - @Deprecated('Use imagePicker.getVideo() method instead.') - static Future pickVideo( - {@required ImageSource source, - CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration}) async { - String path = await platform.pickVideoPath( - source: source, - preferredCameraDevice: preferredCameraDevice, - maxDuration: maxDuration, - ); - - return path == null ? null : File(path); - } - /// Returns a [PickedFile] object wrapping the video that was picked. /// /// The returned [PickedFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. @@ -158,10 +86,10 @@ class ImagePicker { /// /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost /// in this call. You can then call [getLostData] when your app relaunches to retrieve the lost data. - Future getVideo({ - @required ImageSource source, + Future getVideo({ + required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, + Duration? maxDuration, }) { return platform.pickVideo( source: source, @@ -170,23 +98,6 @@ class ImagePicker { ); } - /// Retrieve the lost image file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only) - /// - /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. - /// Call this method to retrieve the lost data and process the data according to your APP's business logic. - /// - /// Returns a [LostDataResponse] if successfully retrieved the lost data. The [LostDataResponse] can represent either a - /// successful image/video selection, or a failure. - /// - /// Calling this on a non-Android platform will throw [UnimplementedError] exception. - /// - /// See also: - /// * [LostDataResponse], for what's included in the response. - /// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction. - static Future retrieveLostData() { - return platform.retrieveLostDataAsDartIoFile(); - } - /// Retrieve the lost [PickedFile] when [selectImage] or [selectVideo] failed because the MainActivity is destroyed. (Android only) /// /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 2dcd7c137b7d..96881e6f2c65 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.7.0 flutter: plugin: @@ -16,17 +16,18 @@ flutter: dependencies: flutter: sdk: flutter - flutter_plugin_android_lifecycle: ^1.0.2 - image_picker_platform_interface: ^1.1.0 + flutter_plugin_android_lifecycle: ^2.0.0 + image_picker_platform_interface: ^2.0.0 dev_dependencies: - video_player: ^0.10.3 flutter_test: sdk: flutter integration_test: path: ../../integration_test - pedantic: ^1.8.0 + mockito: ^5.0.0-nullsafety.7 + pedantic: ^1.10.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.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..0508d257016d 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); @@ -298,7 +310,7 @@ void main() { }); final LostData response = await picker.getLostData(); expect(response.type, RetrieveType.image); - expect(response.file.path, '/example/path'); + expect(response.file!.path, '/example/path'); }); test('retrieveLostData get error response', () async { @@ -311,8 +323,8 @@ void main() { }); final LostData response = await picker.getLostData(); expect(response.type, RetrieveType.video); - expect(response.exception.code, 'test_error_code'); - expect(response.exception.message, 'test_error_message'); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); }); test('retrieveLostData get null response', () async { @@ -336,3 +348,7 @@ void main() { }); }); } + +class MockPlatform extends Mock + with MockPlatformInterfaceMixin + implements ImagePickerPlatform {} diff --git a/packages/image_picker/image_picker/test/old_image_picker_test.dart b/packages/image_picker/image_picker/test/old_image_picker_test.dart deleted file mode 100644 index 8d4e068a261c..000000000000 --- a/packages/image_picker/image_picker/test/old_image_picker_test.dart +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:image_picker/image_picker.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$ImagePicker', () { - const MethodChannel channel = - MethodChannel('plugins.flutter.io/image_picker'); - - final List log = []; - - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return ''; - }); - - log.clear(); - }); - - group('#pickImage', () { - test('passes the image source argument correctly', () async { - await ImagePicker.pickImage(source: ImageSource.camera); - await ImagePicker.pickImage(source: ImageSource.gallery); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 1, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - ], - ); - }); - - test('passes the width and height arguments correctly', () async { - await ImagePicker.pickImage(source: ImageSource.camera); - await ImagePicker.pickImage( - source: ImageSource.camera, - maxWidth: 10.0, - ); - await ImagePicker.pickImage( - source: ImageSource.camera, - maxHeight: 10.0, - ); - await ImagePicker.pickImage( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - ); - await ImagePicker.pickImage( - source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70); - await ImagePicker.pickImage( - source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70); - await ImagePicker.pickImage( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - imageQuality: 70); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - ], - ); - }); - - test('does not accept a negative width or height argument', () { - expect( - ImagePicker.pickImage(source: ImageSource.camera, maxWidth: -1.0), - throwsArgumentError, - ); - - expect( - ImagePicker.pickImage(source: ImageSource.camera, maxHeight: -1.0), - throwsArgumentError, - ); - }); - - test('handles a null image path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); - - expect( - await ImagePicker.pickImage(source: ImageSource.gallery), isNull); - expect(await ImagePicker.pickImage(source: ImageSource.camera), isNull); - }); - - test('camera position defaults to back', () async { - await ImagePicker.pickImage(source: ImageSource.camera); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - }), - ], - ); - }); - - test('camera position can set to front', () async { - await ImagePicker.pickImage( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.front); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 1, - }), - ], - ); - }); - }); - - group('#pickVideo', () { - test('passes the image source argument correctly', () async { - await ImagePicker.pickVideo(source: ImageSource.camera); - await ImagePicker.pickVideo(source: ImageSource.gallery); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - isMethodCall('pickVideo', arguments: { - 'source': 1, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); - }); - - test('passes the duration argument correctly', () async { - await ImagePicker.pickVideo(source: ImageSource.camera); - await ImagePicker.pickVideo( - source: ImageSource.camera, - maxDuration: const Duration(seconds: 10)); - await ImagePicker.pickVideo( - source: ImageSource.camera, - maxDuration: const Duration(minutes: 1)); - await ImagePicker.pickVideo( - source: ImageSource.camera, maxDuration: const Duration(hours: 1)); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 10, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 60, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 3600, - 'cameraDevice': 0, - }), - ], - ); - }); - - test('handles a null video path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); - - expect( - await ImagePicker.pickVideo(source: ImageSource.gallery), isNull); - expect(await ImagePicker.pickVideo(source: ImageSource.camera), isNull); - }); - - test('camera position defaults to back', () async { - await ImagePicker.pickVideo(source: ImageSource.camera); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); - }); - - test('camera position can set to front', () async { - await ImagePicker.pickVideo( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.front); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 1, - }), - ], - ); - }); - }); - - group('#retrieveLostData', () { - test('retrieveLostData get success response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path', - }; - }); - final LostDataResponse response = await ImagePicker.retrieveLostData(); - expect(response.type, RetrieveType.image); - expect(response.file.path, '/example/path'); - }); - - test('retrieveLostData get error response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - }; - }); - final LostDataResponse response = await ImagePicker.retrieveLostData(); - expect(response.type, RetrieveType.video); - expect(response.exception.code, 'test_error_code'); - expect(response.exception.message, 'test_error_message'); - }); - - test('retrieveLostData get null response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return null; - }); - expect((await ImagePicker.retrieveLostData()).isEmpty, true); - }); - - test('retrieveLostData get both path and error should throw', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - 'path': '/example/path', - }; - }); - expect(ImagePicker.retrieveLostData(), throwsAssertionError); - }); - }); - }); -} diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index 604314240a1e..7b2c4077e28d 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,3 +1,12 @@ +# 2.0.0 + +* Migrate to null safety. +* Add doc comments to point out that some arguments aren't supported on the web. + +# 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/README.md b/packages/image_picker/image_picker_for_web/README.md index 81452e290984..8c9f2c73b8fe 100644 --- a/packages/image_picker/image_picker_for_web/README.md +++ b/packages/image_picker/image_picker_for_web/README.md @@ -2,10 +2,10 @@ A web implementation of [`image_picker`][1]. -## Browser Support +## Limitations on the web platform Since Web Browsers don't offer direct access to their users' file system, -this plugin provides a `PickedFile` abstraction to make access access uniform +this plugin provides a `PickedFile` abstraction to make access uniform across platforms. The web version of the plugin puts network-accessible URIs as the `path` @@ -42,6 +42,12 @@ In order to "take a photo", some mobile browsers offer a [`capture` attribute](h Each browser may implement `capture` any way they please, so it may (or may not) make a difference in your users' experience. +### pickImage() +The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported on the web. + +### pickVideo() +The argument `maxDuration` is not supported on the web. + ## Usage ### Import the package diff --git a/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec b/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec deleted file mode 100644 index 23fb795d1cc2..000000000000 --- a/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec +++ /dev/null @@ -1,20 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'image_picker_for_web' - s.version = '0.0.1' - s.summary = 'No-op implementation of image_picker_for_web plugin to avoid build issues on iOS' - s.description = <<-DESC -temp fake image_picker_for_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index e50b4aad3c8d..05afd2e54a4c 100644 --- a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -13,14 +13,14 @@ final String _kAcceptVideoMimeType = 'video/3gpp,video/x-m4v,video/mp4,video/*'; /// /// This class implements the `package:image_picker` functionality for the web. class ImagePickerPlugin extends ImagePickerPlatform { - final ImagePickerPluginTestOverrides _overrides; + final ImagePickerPluginTestOverrides? _overrides; bool get _hasOverrides => _overrides != null; - html.Element _target; + late html.Element _target; /// A constructor that allows tests to override the function that creates file inputs. ImagePickerPlugin({ - @visibleForTesting ImagePickerPluginTestOverrides overrides, + @visibleForTesting ImagePickerPluginTestOverrides? overrides, }) : _overrides = overrides { _target = _ensureInitialized(_kImagePickerInputsDomId); } @@ -30,25 +30,49 @@ class ImagePickerPlugin extends ImagePickerPlatform { ImagePickerPlatform.instance = ImagePickerPlugin(); } + /// Returns a [PickedFile] with the image that was picked. + /// + /// The `source` argument controls where the image comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// Note that the `maxWidth`, `maxHeight` and `imageQuality` arguments are not supported on the web. If any of these arguments is supplied, it'll be silently ignored by the web version of the plugin. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. + /// + /// If no images were picked, the return value is null. @override Future pickImage({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { - String capture = computeCaptureAttribute(source, preferredCameraDevice); + String? capture = computeCaptureAttribute(source, preferredCameraDevice); return pickFile(accept: _kAcceptImageMimeType, capture: capture); } + /// Returns a [PickedFile] containing the video that was picked. + /// + /// The [source] argument controls where the video comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// Note that the `maxDuration` argument is not supported on the web. If the argument is supplied, it'll be silently ignored by the web version of the plugin. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. + /// + /// If no images were picked, the return value is null. @override Future pickVideo({ - @required ImageSource source, + required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, + Duration? maxDuration, }) { - String capture = computeCaptureAttribute(source, preferredCameraDevice); + String? capture = computeCaptureAttribute(source, preferredCameraDevice); return pickFile(accept: _kAcceptVideoMimeType, capture: capture); } @@ -59,10 +83,11 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// See https://caniuse.com/#feat=html-media-capture @visibleForTesting Future pickFile({ - String accept, - String capture, + String? accept, + String? capture, }) { - html.FileUploadInputElement input = createInputElement(accept, capture); + html.FileUploadInputElement input = + createInputElement(accept, capture) as html.FileUploadInputElement; _injectAndActivate(input); return _getSelectedFile(input); } @@ -73,25 +98,26 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// /// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#capture @visibleForTesting - String computeCaptureAttribute(ImageSource source, CameraDevice device) { + String? computeCaptureAttribute(ImageSource source, CameraDevice device) { if (source == ImageSource.camera) { return (device == CameraDevice.front) ? 'user' : 'environment'; } return null; } - html.File _getFileFromInput(html.FileUploadInputElement input) { + html.File? _getFileFromInput(html.FileUploadInputElement input) { if (_hasOverrides) { - return _overrides.getFileFromInput(input); + return _overrides!.getFileFromInput(input); } - return input?.files?.first; + return input.files?.first; } /// Handles the OnChange event from a FileUploadInputElement object /// Returns the objectURL of the selected file. - String _handleOnChangeEvent(html.Event event) { - final html.FileUploadInputElement input = event?.target; - final html.File file = _getFileFromInput(input); + String? _handleOnChangeEvent(html.Event event) { + final html.FileUploadInputElement input = + event.target as html.FileUploadInputElement; + final html.File? file = _getFileFromInput(input); if (file != null) { return html.Url.createObjectUrl(file); @@ -105,7 +131,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { // Observe the input until we can return something input.onChange.first.then((event) { final objectUrl = _handleOnChangeEvent(event); - if (!_completer.isCompleted) { + if (!_completer.isCompleted && objectUrl != null) { _completer.complete(PickedFile(objectUrl)); } }); @@ -127,7 +153,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { final html.Element targetElement = html.Element.tag('flt-image-picker-inputs')..id = id; - html.querySelector('body').children.add(targetElement); + html.querySelector('body')!.children.add(targetElement); target = targetElement; } return target; @@ -136,9 +162,9 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Creates an input element that accepts certain file types, and /// allows to `capture` from the device's cameras (where supported) @visibleForTesting - html.Element createInputElement(String accept, String capture) { + html.Element createInputElement(String? accept, String? capture) { if (_hasOverrides) { - return _overrides.createInputElement(accept, capture); + return _overrides!.createInputElement(accept, capture); } html.Element element = html.FileUploadInputElement()..accept = accept; @@ -162,22 +188,22 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// A function that creates a file input with the passed in `accept` and `capture` attributes. @visibleForTesting typedef OverrideCreateInputFunction = html.Element Function( - String accept, - String capture, + String? accept, + String? capture, ); /// A function that extracts a [html.File] from the file `input` passed in. @visibleForTesting typedef OverrideExtractFilesFromInputFunction = html.File Function( - html.Element input, + html.Element? input, ); /// Overrides for some of the functionality above. @visibleForTesting class ImagePickerPluginTestOverrides { /// Override the creation of the input element. - OverrideCreateInputFunction createInputElement; + late OverrideCreateInputFunction createInputElement; /// Override the extraction of the selected file from an input element. - OverrideExtractFilesFromInputFunction getFileFromInput; + late OverrideExtractFilesFromInputFunction getFileFromInput; } diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 32e89437415e..045be48eb1c5 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -1,10 +1,8 @@ name: image_picker_for_web description: Web platform implementation of image_picker homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web -# 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: 2.0.0 flutter: plugin: @@ -14,19 +12,18 @@ flutter: fileName: image_picker_for_web.dart dependencies: - image_picker_platform_interface: ^1.1.0 + image_picker_platform_interface: ^2.0.0 + meta: ^1.3.0 flutter: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 - js: ^0.6.0 dev_dependencies: + pedantic: ^1.10.0 flutter_test: sdk: flutter - pedantic: ^1.8.0 environment: - sdk: ">=2.5.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart index 96d048dd2a8e..fcc2c003a10b 100644 --- a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart @@ -13,12 +13,12 @@ import 'package:image_picker_for_web/image_picker_for_web.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; final String expectedStringContents = "Hello, world!"; -final Uint8List bytes = utf8.encode(expectedStringContents); +final Uint8List bytes = utf8.encode(expectedStringContents) as Uint8List; final html.File textFile = html.File([bytes], "hello.txt"); void main() { // Under test... - ImagePickerPlugin plugin; + late ImagePickerPlugin plugin; setUp(() { plugin = ImagePickerPlugin(); diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 1ed45028f513..f2e017e190c5 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,22 @@ +## 2.0.0 + +* Migrate to null safety. +* Breaking Changes: + * Removed the deprecated methods: `ImagePickerPlatform.retrieveLostDataAsDartIoFile`,`ImagePickerPlatform.pickImagePath` and `ImagePickerPlatform.pickVideoPath`. + * Removed deprecated class: `LostDataResponse`. + +## 1.1.6 + +* Fix test asset file location. + +## 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/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart index 71704b63ced4..8535f9dfb20e 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart @@ -3,11 +3,10 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import 'package:meta/meta.dart' show required, visibleForTesting; +import 'package:meta/meta.dart' show visibleForTesting; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; @@ -20,14 +19,14 @@ class MethodChannelImagePicker extends ImagePickerPlatform { MethodChannel get channel => _channel; @override - Future pickImage({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - String path = await pickImagePath( + String? path = await _pickImagePath( source: source, maxWidth: maxWidth, maxHeight: maxHeight, @@ -37,15 +36,13 @@ class MethodChannelImagePicker extends ImagePickerPlatform { return path != null ? PickedFile(path) : null; } - @override - Future pickImagePath({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, + Future _pickImagePath({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { - assert(source != null); if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( imageQuality, 'imageQuality', 'must be between 0 and 100'); @@ -72,12 +69,12 @@ class MethodChannelImagePicker extends ImagePickerPlatform { } @override - Future pickVideo({ - @required ImageSource source, + Future pickVideo({ + required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, + Duration? maxDuration, }) async { - String path = await pickVideoPath( + String? path = await _pickVideoPath( source: source, maxDuration: maxDuration, preferredCameraDevice: preferredCameraDevice, @@ -85,13 +82,11 @@ class MethodChannelImagePicker extends ImagePickerPlatform { return path != null ? PickedFile(path) : null; } - @override - Future pickVideoPath({ - @required ImageSource source, + Future _pickVideoPath({ + required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, + Duration? maxDuration, }) { - assert(source != null); return _channel.invokeMethod( 'pickVideo', { @@ -104,7 +99,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { @override Future retrieveLostData() async { - final Map result = + final Map? result = await _channel.invokeMapMethod('retrieve'); if (result == null) { @@ -113,23 +108,23 @@ class MethodChannelImagePicker extends ImagePickerPlatform { assert(result.containsKey('path') ^ result.containsKey('errorCode')); - final String type = result['type']; + final String? type = result['type']; assert(type == kTypeImage || type == kTypeVideo); - RetrieveType retrieveType; + RetrieveType? retrieveType; if (type == kTypeImage) { retrieveType = RetrieveType.image; } else if (type == kTypeVideo) { retrieveType = RetrieveType.video; } - PlatformException exception; + PlatformException? exception; if (result.containsKey('errorCode')) { exception = PlatformException( code: result['errorCode'], message: result['errorMessage']); } - final String path = result['path']; + final String? path = result['path']; return LostData( file: path != null ? PickedFile(path) : null, @@ -137,40 +132,4 @@ class MethodChannelImagePicker extends ImagePickerPlatform { type: retrieveType, ); } - - @override - // ignore: deprecated_member_use_from_same_package - Future retrieveLostDataAsDartIoFile() async { - final Map result = - await _channel.invokeMapMethod('retrieve'); - if (result == null) { - // ignore: deprecated_member_use_from_same_package - return LostDataResponse.empty(); - } - assert(result.containsKey('path') ^ result.containsKey('errorCode')); - - final String type = result['type']; - assert(type == kTypeImage || type == kTypeVideo); - - RetrieveType retrieveType; - if (type == kTypeImage) { - retrieveType = RetrieveType.image; - } else if (type == kTypeVideo) { - retrieveType = RetrieveType.video; - } - - PlatformException exception; - if (result.containsKey('errorCode')) { - exception = PlatformException( - code: result['errorCode'], message: result['errorMessage']); - } - - final String path = result['path']; - - // ignore: deprecated_member_use_from_same_package - return LostDataResponse( - file: path == null ? null : File(path), - exception: exception, - type: retrieveType); - } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart index cbd604187714..44b85f17f3db 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart @@ -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:image_picker_platform_interface/src/method_channel/method_channel_image_picker.dart'; @@ -39,80 +38,6 @@ abstract class ImagePickerPlatform extends PlatformInterface { _instance = instance; } - /// Returns a [String] containing a path to the image that was picked. - /// - /// The `source` argument controls where the image comes from. This can - /// be either [ImageSource.camera] or [ImageSource.gallery]. - /// - /// If specified, the image will be at most `maxWidth` wide and - /// `maxHeight` tall. Otherwise the image will be returned at it's - /// original width and height. - /// - /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 - /// where 100 is the original/max quality. If `imageQuality` is null, the image with - /// the original quality will be returned. Compression is only supported for certain - /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, - /// a warning message will be logged. - /// - /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. - /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. - /// Defaults to [CameraDevice.rear]. - /// - /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost - /// in this call. You can then call [retrieveLostDataAsDartIoFile] when your app relaunches to retrieve the lost data. - @Deprecated('Use pickImage instead.') - Future pickImagePath({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, - CameraDevice preferredCameraDevice = CameraDevice.rear, - }) { - throw UnimplementedError('legacyPickImage() has not been implemented.'); - } - - /// Returns a [String] containing a path to the video that was picked. - /// - /// The [source] argument controls where the video comes from. This can - /// be either [ImageSource.camera] or [ImageSource.gallery]. - /// - /// The [maxDuration] argument specifies the maximum duration of the captured video. If no [maxDuration] is specified, - /// the maximum duration will be infinite. - /// - /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. - /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. - /// Defaults to [CameraDevice.rear]. - /// - /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost - /// in this call. You can then call [retrieveLostDataAsDartIoFile] when your app relaunches to retrieve the lost data. - @Deprecated('Use pickVideo instead.') - Future pickVideoPath({ - @required ImageSource source, - CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, - }) { - throw UnimplementedError('pickVideoPath() has not been implemented.'); - } - - /// Retrieve the lost image file when [pickImagePath] or [pickVideoPath] failed because the MainActivity is destroyed. (Android only) - /// - /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. - /// Call this method to retrieve the lost data and process the data according to your APP's business logic. - /// - /// Returns a [LostDataResponse] if successfully retrieved the lost data. The [LostDataResponse] can represent either a - /// successful image/video selection, or a failure. - /// - /// Calling this on a non-Android platform will throw [UnimplementedError] exception. - /// - /// See also: - /// * [LostDataResponse], for what's included in the response. - /// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction. - @Deprecated('Use retrieveLostData instead.') - Future retrieveLostDataAsDartIoFile() { - throw UnimplementedError( - 'retrieveLostDataAsDartIoFile() has not been implemented.'); - } - // Next version of the API. /// Returns a [PickedFile] with the image that was picked. @@ -141,11 +66,13 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. - Future pickImage({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, + /// + /// If no images were picked, the return value is null. + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { throw UnimplementedError('pickImage() has not been implemented.'); @@ -165,10 +92,12 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. - Future pickVideo({ - @required ImageSource source, + /// + /// If no images were picked, the return value is null. + Future pickVideo({ + required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, + Duration? maxDuration, }) { throw UnimplementedError('pickVideo() has not been implemented.'); } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart deleted file mode 100644 index d82618b23cd1..000000000000 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart +++ /dev/null @@ -1,52 +0,0 @@ -// 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:io'; - -import 'package:flutter/services.dart'; -import 'package:image_picker_platform_interface/src/types/types.dart'; - -/// The response object of [ImagePicker.retrieveLostData]. -/// -/// Only applies to Android. -/// See also: -/// * [ImagePicker.retrieveLostData] for more details on retrieving lost data. -@Deprecated('Use methods that return a LostData object instead.') -class LostDataResponse { - /// Creates an instance with the given [file], [exception], and [type]. Any of - /// the params may be null, but this is never considered to be empty. - LostDataResponse({this.file, this.exception, this.type}); - - /// Initializes an instance with all member params set to null and considered - /// to be empty. - LostDataResponse.empty() - : file = null, - exception = null, - type = null, - _empty = true; - - /// Whether it is an empty response. - /// - /// An empty response should have [file], [exception] and [type] to be null. - bool get isEmpty => _empty; - - /// The file that was lost in a previous [pickImage] or [pickVideo] call due to MainActivity being destroyed. - /// - /// Can be null if [exception] exists. - final File file; - - /// The exception of the last [pickImage] or [pickVideo]. - /// - /// If the last [pickImage] or [pickVideo] threw some exception before the MainActivity destruction, this variable keeps that - /// exception. - /// You should handle this exception as if the [pickImage] or [pickVideo] got an exception when the MainActivity was not destroyed. - /// - /// Note that it is not the exception that caused the destruction of the MainActivity. - final PlatformException exception; - - /// Can either be [RetrieveType.image] or [RetrieveType.video]; - final RetrieveType type; - - bool _empty = false; -} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart index 285294efcb3d..2b078ef28190 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart @@ -52,7 +52,7 @@ abstract class PickedFileBase { /// 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]) { + Stream openRead([int? start, int? end]) { throw UnimplementedError('openRead() has not been implemented.'); } } 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..b855eb3fa20d 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 @@ -10,21 +10,21 @@ import './base.dart'; /// It wraps the bytes of a selected file. class PickedFile extends PickedFileBase { final String path; - final Uint8List _initBytes; + final Uint8List? _initBytes; /// Construct a PickedFile object from its ObjectUrl. /// /// Optionally, this can be initialized with `bytes` /// so no http requests are performed to retrieve files later. - PickedFile(this.path, {Uint8List bytes}) + PickedFile(this.path, {Uint8List? bytes}) : _initBytes = bytes, super(path); Future get _bytes async { if (_initBytes != null) { - return Future.value(UnmodifiableUint8ListView(_initBytes)); + return Future.value(UnmodifiableUint8ListView(_initBytes!)); } - return http.readBytes(path); + return http.readBytes(Uri.parse(path)); } @override @@ -38,7 +38,7 @@ class PickedFile extends PickedFileBase { } @override - Stream openRead([int start, int end]) async* { + Stream openRead([int? start, int? end]) async* { final bytes = await _bytes; yield bytes.sublist(start ?? 0, end ?? bytes.length); } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart index dd64558bf044..4b56add0add4 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart @@ -29,7 +29,7 @@ class PickedFile extends PickedFileBase { } @override - Stream openRead([int start, int end]) { + Stream openRead([int? start, int? end]) { return _file .openRead(start ?? 0, end) .map((chunk) => Uint8List.fromList(chunk)); diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart index b94e69de219e..831c7bd6cb15 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart @@ -31,7 +31,7 @@ class LostData { /// The file that was lost in a previous [pickImage] or [pickVideo] call due to MainActivity being destroyed. /// /// Can be null if [exception] exists. - final PickedFile file; + final PickedFile? file; /// The exception of the last [pickImage] or [pickVideo]. /// @@ -40,10 +40,12 @@ class LostData { /// You should handle this exception as if the [pickImage] or [pickVideo] got an exception when the MainActivity was not destroyed. /// /// Note that it is not the exception that caused the destruction of the MainActivity. - final PlatformException exception; + final PlatformException? exception; /// Can either be [RetrieveType.image] or [RetrieveType.video]; - final RetrieveType type; + /// + /// If the lost data is empty, this will be null. + final RetrieveType? type; bool _empty = false; } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart index 9c44fae1aa9d..f38a4ec74005 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart @@ -1,6 +1,5 @@ export 'camera_device.dart'; export 'image_source.dart'; -export 'lost_data_response.dart'; export 'retrieve_type.dart'; export 'picked_file/picked_file.dart'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 39a65284e247..9befba90215a 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -3,21 +3,20 @@ 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: 2.0.0 dependencies: flutter: sdk: flutter - meta: ^1.1.8 - http: ^0.12.1 - plugin_platform_interface: ^1.0.2 + meta: ^1.3.0 + http: ^0.13.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0+1 + pedantic: ^1.10.0 environment: - sdk: ">=2.5.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.10.0" diff --git a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart deleted file mode 100644 index ddaad3d32f41..000000000000 --- a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart +++ /dev/null @@ -1,345 +0,0 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -import 'package:image_picker_platform_interface/src/method_channel/method_channel_image_picker.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$MethodChannelImagePicker', () { - MethodChannelImagePicker picker = MethodChannelImagePicker(); - - final List log = []; - - setUp(() { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return ''; - }); - - log.clear(); - }); - - group('#pickImagePath', () { - test('passes the image source argument correctly', () async { - await picker.pickImagePath(source: ImageSource.camera); - await picker.pickImagePath(source: ImageSource.gallery); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 1, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - ], - ); - }); - - test('passes the width and height arguments correctly', () async { - await picker.pickImagePath(source: ImageSource.camera); - await picker.pickImagePath( - source: ImageSource.camera, - maxWidth: 10.0, - ); - await picker.pickImagePath( - source: ImageSource.camera, - maxHeight: 10.0, - ); - await picker.pickImagePath( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - ); - await picker.pickImagePath( - source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70); - await picker.pickImagePath( - source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70); - await picker.pickImagePath( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - imageQuality: 70); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - ], - ); - }); - - test('does not accept a negative width or height argument', () { - expect( - () => - picker.pickImagePath(source: ImageSource.camera, maxWidth: -1.0), - throwsArgumentError, - ); - - expect( - () => - picker.pickImagePath(source: ImageSource.camera, maxHeight: -1.0), - throwsArgumentError, - ); - }); - - test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); - - expect(await picker.pickImagePath(source: ImageSource.gallery), isNull); - expect(await picker.pickImagePath(source: ImageSource.camera), isNull); - }); - - test('camera position defaults to back', () async { - await picker.pickImagePath(source: ImageSource.camera); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - }), - ], - ); - }); - - test('camera position can set to front', () async { - await picker.pickImagePath( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.front); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 1, - }), - ], - ); - }); - }); - - group('#pickVideoPath', () { - test('passes the image source argument correctly', () async { - await picker.pickVideoPath(source: ImageSource.camera); - await picker.pickVideoPath(source: ImageSource.gallery); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - isMethodCall('pickVideo', arguments: { - 'source': 1, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); - }); - - test('passes the duration argument correctly', () async { - await picker.pickVideoPath(source: ImageSource.camera); - await picker.pickVideoPath( - source: ImageSource.camera, - maxDuration: const Duration(seconds: 10)); - await picker.pickVideoPath( - source: ImageSource.camera, - maxDuration: const Duration(minutes: 1)); - await picker.pickVideoPath( - source: ImageSource.camera, maxDuration: const Duration(hours: 1)); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 10, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 60, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 3600, - 'cameraDevice': 0, - }), - ], - ); - }); - - test('handles a null video path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); - - expect(await picker.pickVideoPath(source: ImageSource.gallery), isNull); - expect(await picker.pickVideoPath(source: ImageSource.camera), isNull); - }); - - test('camera position defaults to back', () async { - await picker.pickVideoPath(source: ImageSource.camera); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); - }); - - test('camera position can set to front', () async { - await picker.pickVideoPath( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.front); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 1, - }), - ], - ); - }); - }); - - group('#retrieveLostDataAsDartIoFile', () { - test('retrieveLostData get success response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path', - }; - }); - // ignore: deprecated_member_use_from_same_package - final LostDataResponse response = - await picker.retrieveLostDataAsDartIoFile(); - expect(response.type, RetrieveType.image); - expect(response.file.path, '/example/path'); - }); - - test('retrieveLostData get error response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - }; - }); - // ignore: deprecated_member_use_from_same_package - final LostDataResponse response = - await picker.retrieveLostDataAsDartIoFile(); - expect(response.type, RetrieveType.video); - expect(response.exception.code, 'test_error_code'); - expect(response.exception.message, 'test_error_message'); - }); - - test('retrieveLostData get null response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return null; - }); - expect((await picker.retrieveLostDataAsDartIoFile()).isEmpty, true); - }); - - test('retrieveLostData get both path and error should throw', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - 'path': '/example/path', - }; - }); - expect(picker.retrieveLostDataAsDartIoFile(), throwsAssertionError); - }); - }, skip: isBrowser); - }); -} diff --git a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart index e7abe37e4838..df35f8fd96b8 100644 --- a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart @@ -312,7 +312,8 @@ void main() { // ignore: deprecated_member_use_from_same_package final LostData response = await picker.retrieveLostData(); expect(response.type, RetrieveType.image); - expect(response.file.path, '/example/path'); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path'); }); test('retrieveLostData get error response', () async { @@ -326,8 +327,9 @@ void main() { // ignore: deprecated_member_use_from_same_package final LostData response = await picker.retrieveLostData(); expect(response.type, RetrieveType.video); - expect(response.exception.code, 'test_error_code'); - expect(response.exception.message, 'test_error_message'); + expect(response.exception, isNotNull); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); }); test('retrieveLostData get null response', () async { diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart index 49d84ff88f88..ee0edbea03e0 100644 --- a/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart @@ -6,13 +6,12 @@ import 'dart:convert'; import 'dart:html' as html; -import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; final String expectedStringContents = 'Hello, world!'; -final Uint8List bytes = utf8.encode(expectedStringContents); +final List bytes = utf8.encode(expectedStringContents); final html.File textFile = html.File([bytes], 'hello.txt'); final String textFileUrl = html.Url.createObjectUrl(textFile); diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart index 94ff759a2fb2..28c0886b864e 100644 --- a/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart +++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart @@ -11,14 +11,17 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +final pathPrefix = + Directory.current.path.endsWith('test') ? './assets/' : './test/assets/'; +final path = pathPrefix + 'hello.txt'; final String expectedStringContents = 'Hello, world!'; -final Uint8List bytes = utf8.encode(expectedStringContents); -final File textFile = File('./assets/hello.txt'); +final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents)); +final File textFile = File(path); final String textFilePath = textFile.path; void main() { group('Create with an objectUrl', () { - final pickedFile = PickedFile(textFilePath); + final PickedFile pickedFile = PickedFile(textFilePath); test('Can be read as a string', () async { expect(await pickedFile.readAsString(), equals(expectedStringContents)); diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index 6207da9b403c..0dbc2427ccd6 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,3 +1,34 @@ +## 0.4.1 + +* Support InApp subscription upgrade/downgrade. + +## 0.4.0 + +* Migrate to nullsafety. +* Deprecate `sandboxTesting`, introduce `simulatesAskToBuyInSandbox`. +* **Breaking Change:** + * Removed `callbackChannel` in `channels.dart`, see https://github.com/flutter/flutter/issues/69225. + +## 0.3.5+2 + +* Migrate deprecated references. + +## 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..321864b2a233 100644 --- a/packages/in_app_purchase/README.md +++ b/packages/in_app_purchase/README.md @@ -178,10 +178,34 @@ and `AppStore` that the purchase has been finished. WARNING! Failure to call `InAppPurchaseConnection.completePurchase` and get a successful response within 3 days of the purchase will result a refund. +### Upgrading or Downgrading an existing InApp Subscription + +In order to upgrade/downgrade an existing InApp subscription on `PlayStore`, +you need to provide an instance of `ChangeSubscriptionParam` with the old +`PurchaseDetails` that the user needs to migrate from, and an optional `ProrationMode` +with the `PurchaseParam` object while calling `InAppPurchaseConnection.buyNonConsumable`. +`AppStore` does not require this since they provides a subscription grouping mechanism. +Each subscription you offer must be assigned to a subscription group. +So the developers can group related subscriptions together to prevents users from +accidentally purchasing multiple subscriptions. +Please refer to the 'Creating a Subscription Group' sections of [Apple's subscription guide](https://developer.apple.com/app-store/subscriptions/) + + +```dart +final PurchaseDetails oldPurchaseDetails = ...; +PurchaseParam purchaseParam = PurchaseParam( + productDetails: productDetails, + changeSubscriptionParam: ChangeSubscriptionParam( + oldPurchaseDetails: oldPurchaseDetails, + prorationMode: ProrationMode.immediateWithTimeProration)); +InAppPurchaseConnection.instance + .buyNonConsumable(purchaseParam: purchaseParam); +``` + ## 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/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java index f1e715e239a2..58d077673a03 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -20,6 +20,7 @@ import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingClientStateListener; import com.android.billingclient.api.BillingFlowParams; +import com.android.billingclient.api.BillingFlowParams.ProrationMode; import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.ConsumeResponseListener; @@ -39,6 +40,8 @@ class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler, Application.ActivityLifecycleCallbacks { private static final String TAG = "InAppPurchasePlugin"; + private static final String LOAD_SKU_DOC_URL = + "https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/README.md#loading-products-for-sale"; @Nullable private BillingClient billingClient; private final BillingClientFactory billingClientFactory; @@ -120,7 +123,13 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { break; case InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW: launchBillingFlow( - (String) call.argument("sku"), (String) call.argument("accountId"), result); + (String) call.argument("sku"), + (String) call.argument("accountId"), + (String) call.argument("oldSku"), + call.hasArgument("prorationMode") + ? (int) call.argument("prorationMode") + : ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY, + result); break; case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES: queryPurchases((String) call.argument("skuType"), result); @@ -189,7 +198,11 @@ public void onSkuDetailsResponse( } private void launchBillingFlow( - String sku, @Nullable String accountId, MethodChannel.Result result) { + String sku, + @Nullable String accountId, + @Nullable String oldSku, + int prorationMode, + MethodChannel.Result result) { if (billingClientError(result)) { return; } @@ -198,7 +211,26 @@ private void launchBillingFlow( if (skuDetails == null) { result.error( "NOT_FOUND", - "Details for sku " + sku + " are not available. Has this ID already been fetched?", + String.format( + "Details for sku %s are not available. It might because skus were not fetched prior to the call. Please fetch the skus first. An example of how to fetch the skus could be found here: %s", + sku, LOAD_SKU_DOC_URL), + null); + return; + } + + if (oldSku == null + && prorationMode != ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) { + result.error( + "IN_APP_PURCHASE_REQUIRE_OLD_SKU", + "launchBillingFlow failed because oldSku is null. You must provide a valid oldSku in order to use a proration mode.", + null); + return; + } else if (oldSku != null && !cachedSkus.containsKey(oldSku)) { + result.error( + "IN_APP_PURCHASE_INVALID_OLD_SKU", + String.format( + "Details for sku %s are not available. It might because skus were not fetched prior to the call. Please fetch the skus first. An example of how to fetch the skus could be found here: %s", + oldSku, LOAD_SKU_DOC_URL), null); return; } @@ -218,6 +250,12 @@ private void launchBillingFlow( if (accountId != null && !accountId.isEmpty()) { paramsBuilder.setAccountId(accountId); } + if (oldSku != null && !oldSku.isEmpty()) { + paramsBuilder.setOldSku(oldSku); + } + // The proration mode value has to match one of the following declared in + // https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode + paramsBuilder.setReplaceSkusProrationMode(prorationMode); result.success( Translator.fromBillingResult( billingClient.launchBillingFlow(activity, paramsBuilder.build()))); @@ -252,7 +290,8 @@ private void queryPurchases(String skuType, MethodChannel.Result result) { return; } - // Like in our connect call, consider the billing client responding a "success" here regardless of status code. + // Like in our connect call, consider the billing client responding a "success" here regardless + // of status code. result.success(fromPurchasesResult(billingClient.queryPurchases(skuType))); } @@ -295,7 +334,8 @@ public void onBillingSetupFinished(BillingResult billingResult) { return; } alreadyFinished = true; - // Consider the fact that we've finished a success, leave it to the Dart side to validate the responseCode. + // Consider the fact that we've finished a success, leave it to the Dart side to + // validate the responseCode. result.success(Translator.fromBillingResult(billingResult)); } diff --git a/packages/in_app_purchase/build.yaml b/packages/in_app_purchase/build.yaml index d7b59734f27e..e15cf14b85fd 100644 --- a/packages/in_app_purchase/build.yaml +++ b/packages/in_app_purchase/build.yaml @@ -5,4 +5,3 @@ targets: options: any_map: true create_to_json: true - nullable: false \ No newline at end of file diff --git a/packages/in_app_purchase/example/README.md b/packages/in_app_purchase/example/README.md index 9fcad23d19ae..6dd5b38d7003 100644 --- a/packages/in_app_purchase/example/README.md +++ b/packages/in_app_purchase/example/README.md @@ -30,7 +30,8 @@ below. - `consumable`: A managed product. - `upgrade`: A managed product. - - `subscription`: A subscription. + - `subscription_silver`: A lower level subscription. + - `subscription_gold`: A higher level subscription. Make sure that all of the products are set to `ACTIVE`. diff --git a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index c6a9b4114a75..cc7bc4a9b9b1 100644 --- a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ b/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -18,6 +18,7 @@ import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; +import static java.util.Collections.unmodifiableList; import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -261,7 +262,7 @@ public void querySkuDetailsAsync_clientDisconnected() { } @Test - public void launchBillingFlow_ok_nullAccountId() { + public void launchBillingFlow_ok_null_AccountId() { // Fetch the sku details first and then prepare the launch billing flow call String skuId = "foo"; queryForSkus(singletonList(skuId)); @@ -292,6 +293,40 @@ public void launchBillingFlow_ok_nullAccountId() { verify(result, times(1)).success(fromBillingResult(billingResult)); } + @Test + public void launchBillingFlow_ok_null_OldSku() { + // Fetch the sku details first and then prepare the launch billing flow call + String skuId = "foo"; + String accountId = "account"; + queryForSkus(singletonList(skuId)); + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + arguments.put("oldSku", null); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + + // Launch the billing flow + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); + methodChannelHandler.onMethodCall(launchCall, result); + + // Verify we pass the arguments to the billing flow + ArgumentCaptor billingFlowParamsCaptor = + ArgumentCaptor.forClass(BillingFlowParams.class); + verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); + BillingFlowParams params = billingFlowParamsCaptor.getValue(); + assertEquals(params.getSku(), skuId); + assertEquals(params.getAccountId(), accountId); + assertNull(params.getOldSku()); + // Verify we pass the response code to result + verify(result, never()).error(any(), any(), any()); + verify(result, times(1)).success(fromBillingResult(billingResult)); + } + @Test public void launchBillingFlow_ok_null_Activity() { methodChannelHandler.setActivity(null); @@ -311,6 +346,42 @@ public void launchBillingFlow_ok_null_Activity() { verify(result, never()).success(any()); } + @Test + public void launchBillingFlow_ok_oldSku() { + // Fetch the sku details first and query the method call + String skuId = "foo"; + String accountId = "account"; + String oldSkuId = "oldFoo"; + queryForSkus(unmodifiableList(asList(skuId, oldSkuId))); + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + arguments.put("oldSku", oldSkuId); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + + // Launch the billing flow + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); + methodChannelHandler.onMethodCall(launchCall, result); + + // Verify we pass the arguments to the billing flow + ArgumentCaptor billingFlowParamsCaptor = + ArgumentCaptor.forClass(BillingFlowParams.class); + verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); + BillingFlowParams params = billingFlowParamsCaptor.getValue(); + assertEquals(params.getSku(), skuId); + assertEquals(params.getAccountId(), accountId); + assertEquals(params.getOldSku(), oldSkuId); + + // Verify we pass the response code to result + verify(result, never()).error(any(), any(), any()); + verify(result, times(1)).success(fromBillingResult(billingResult)); + } + @Test public void launchBillingFlow_ok_AccountId() { // Fetch the sku details first and query the method call @@ -344,6 +415,79 @@ public void launchBillingFlow_ok_AccountId() { verify(result, times(1)).success(fromBillingResult(billingResult)); } + @Test + public void launchBillingFlow_ok_Proration() { + // Fetch the sku details first and query the method call + String skuId = "foo"; + String oldSkuId = "oldFoo"; + String accountId = "account"; + int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; + queryForSkus(unmodifiableList(asList(skuId, oldSkuId))); + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + arguments.put("oldSku", oldSkuId); + arguments.put("prorationMode", prorationMode); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + + // Launch the billing flow + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); + methodChannelHandler.onMethodCall(launchCall, result); + + // Verify we pass the arguments to the billing flow + ArgumentCaptor billingFlowParamsCaptor = + ArgumentCaptor.forClass(BillingFlowParams.class); + verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); + BillingFlowParams params = billingFlowParamsCaptor.getValue(); + assertEquals(params.getSku(), skuId); + assertEquals(params.getAccountId(), accountId); + assertEquals(params.getOldSku(), oldSkuId); + assertEquals(params.getReplaceSkusProrationMode(), prorationMode); + + // Verify we pass the response code to result + verify(result, never()).error(any(), any(), any()); + verify(result, times(1)).success(fromBillingResult(billingResult)); + } + + @Test + public void launchBillingFlow_ok_Proration_with_null_OldSku() { + // Fetch the sku details first and query the method call + String skuId = "foo"; + String accountId = "account"; + String queryOldSkuId = "oldFoo"; + String oldSkuId = null; + int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; + queryForSkus(unmodifiableList(asList(skuId, queryOldSkuId))); + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + arguments.put("oldSku", oldSkuId); + arguments.put("prorationMode", prorationMode); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + + // Launch the billing flow + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); + methodChannelHandler.onMethodCall(launchCall, result); + + // Assert that we sent an error back. + verify(result) + .error( + contains("IN_APP_PURCHASE_REQUIRE_OLD_SKU"), + contains("launchBillingFlow failed because oldSku is null"), + any()); + verify(result, never()).success(any()); + } + @Test public void launchBillingFlow_clientDisconnected() { // Prepare the launch call after disconnecting the client @@ -381,6 +525,27 @@ public void launchBillingFlow_skuNotFound() { verify(result, never()).success(any()); } + @Test + public void launchBillingFlow_oldSkuNotFound() { + // Try to launch the billing flow for a random sku ID + establishConnectedBillingClient(null, null); + String skuId = "foo"; + String accountId = "account"; + String oldSkuId = "oldSku"; + queryForSkus(singletonList(skuId)); + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + arguments.put("oldSku", oldSkuId); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + + methodChannelHandler.onMethodCall(launchCall, result); + + // Assert that we sent an error back. + verify(result).error(contains("IN_APP_PURCHASE_INVALID_OLD_SKU"), contains(oldSkuId), any()); + verify(result, never()).success(any()); + } + @Test public void queryPurchases() { establishConnectedBillingClient(null, null); diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart index b285b6e2bb87..c9f0bb6ece25 100644 --- a/packages/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/example/lib/main.dart @@ -19,10 +19,14 @@ void main() { const bool _kAutoConsume = true; const String _kConsumableId = 'consumable'; +const String _kUpgradeId = 'upgrade'; +const String _kSilverSubscriptionId = 'subscription_silver'; +const String _kGoldSubscriptionId = 'subscription_gold'; const List _kProductIds = [ _kConsumableId, - 'upgrade', - 'subscription' + _kUpgradeId, + _kSilverSubscriptionId, + _kGoldSubscriptionId, ]; class _MyApp extends StatefulWidget { @@ -32,7 +36,7 @@ class _MyApp extends StatefulWidget { class _MyAppState extends State<_MyApp> { final InAppPurchaseConnection _connection = InAppPurchaseConnection.instance; - StreamSubscription> _subscription; + late StreamSubscription> _subscription; List _notFoundIds = []; List _products = []; List _purchases = []; @@ -40,11 +44,11 @@ class _MyAppState extends State<_MyApp> { bool _isAvailable = false; bool _purchasePending = false; bool _loading = true; - String _queryProductError; + String? _queryProductError; @override void initState() { - Stream purchaseUpdated = + final Stream> purchaseUpdated = InAppPurchaseConnection.instance.purchaseUpdatedStream; _subscription = purchaseUpdated.listen((purchaseDetailsList) { _listenToPurchaseUpdated(purchaseDetailsList); @@ -76,7 +80,7 @@ class _MyAppState extends State<_MyApp> { await _connection.queryProductDetails(_kProductIds.toSet()); if (productDetailResponse.error != null) { setState(() { - _queryProductError = productDetailResponse.error.message; + _queryProductError = productDetailResponse.error!.message; _isAvailable = isAvailable; _products = productDetailResponse.productDetails; _purchases = []; @@ -146,7 +150,7 @@ class _MyAppState extends State<_MyApp> { ); } else { stack.add(Center( - child: Text(_queryProductError), + child: Text(_queryProductError!), )); } if (_purchasePending) { @@ -235,7 +239,7 @@ class _MyAppState extends State<_MyApp> { })); productList.addAll(_products.map( (ProductDetails productDetails) { - PurchaseDetails previousPurchase = purchases[productDetails.id]; + PurchaseDetails? previousPurchase = purchases[productDetails.id]; return ListTile( title: Text( productDetails.title, @@ -245,15 +249,29 @@ 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: () { + // NOTE: If you are making a subscription purchase/upgrade/downgrade, we recommend you to + // verify the latest status of you your subscription by using server side receipt validation + // and update the UI accordingly. The subscription purchase status shown + // inside the app may not be accurate. + final oldSubscription = + _getOldSubscription(productDetails, purchases); PurchaseParam purchaseParam = PurchaseParam( productDetails: productDetails, applicationUserName: null, - sandboxTesting: true); + changeSubscriptionParam: Platform.isAndroid && + oldSubscription != null + ? ChangeSubscriptionParam( + oldPurchaseDetails: oldSubscription, + prorationMode: + ProrationMode.immediateWithTimeProration) + : null); if (productDetails.id == _kConsumableId) { _connection.buyConsumable( purchaseParam: purchaseParam, @@ -327,7 +345,7 @@ class _MyAppState extends State<_MyApp> { void deliverProduct(PurchaseDetails purchaseDetails) async { // IMPORTANT!! Always verify a purchase purchase details before delivering the product. if (purchaseDetails.productID == _kConsumableId) { - await ConsumableStore.save(purchaseDetails.purchaseID); + await ConsumableStore.save(purchaseDetails.purchaseID!); List consumables = await ConsumableStore.load(); setState(() { _purchasePending = false; @@ -363,7 +381,7 @@ class _MyAppState extends State<_MyApp> { showPendingUI(); } else { if (purchaseDetails.status == PurchaseStatus.error) { - handleError(purchaseDetails.error); + handleError(purchaseDetails.error!); } else if (purchaseDetails.status == PurchaseStatus.purchased) { bool valid = await _verifyPurchase(purchaseDetails); if (valid) { @@ -386,4 +404,24 @@ class _MyAppState extends State<_MyApp> { } }); } + + PurchaseDetails? _getOldSubscription( + ProductDetails productDetails, Map purchases) { + // This is just to demonstrate a subscription upgrade or downgrade. + // This method assumes that you have only 2 subscriptions under a group, 'subscription_silver' & 'subscription_gold'. + // The 'subscription_silver' subscription can be upgraded to 'subscription_gold' and + // the 'subscription_gold' subscription can be downgraded to 'subscription_silver'. + // Please remember to replace the logic of finding the old subscription Id as per your app. + // The old subscription is only required on Android since Apple handles this internally + // by using the subscription group feature in iTunesConnect. + PurchaseDetails? oldSubscription; + if (productDetails.id == _kSilverSubscriptionId && + purchases[_kGoldSubscriptionId] != null) { + oldSubscription = purchases[_kGoldSubscriptionId]; + } else if (productDetails.id == _kGoldSubscriptionId && + purchases[_kSilverSubscriptionId] != null) { + oldSubscription = purchases[_kSilverSubscriptionId]; + } + return oldSubscription; + } } diff --git a/packages/in_app_purchase/example/pubspec.yaml b/packages/in_app_purchase/example/pubspec.yaml index 48359dbc6a06..8c9296dc98c8 100644 --- a/packages/in_app_purchase/example/pubspec.yaml +++ b/packages/in_app_purchase/example/pubspec.yaml @@ -5,22 +5,25 @@ author: Flutter Team dependencies: flutter: sdk: flutter - cupertino_icons: ^0.1.2 - shared_preferences: ^0.5.2 + shared_preferences: ^2.0.0-nullsafety.1 dev_dependencies: - test: ^1.5.2 flutter_driver: sdk: flutter in_app_purchase: + # When depending on this package from a real application you should use: + # in_app_purchase: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ integration_test: path: ../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.9.1+hotfix.2" diff --git a/packages/in_app_purchase/example/test_driver/test/integration_test.dart b/packages/in_app_purchase/example/test_driver/test/integration_test.dart index 7a2c21338786..0352d4aaeb2d 100644 --- a/packages/in_app_purchase/example/test_driver/test/integration_test.dart +++ b/packages/in_app_purchase/example/test_driver/test/integration_test.dart @@ -2,6 +2,7 @@ // 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/in_app_purchase/integration_test/in_app_purchase_test.dart b/packages/in_app_purchase/integration_test/in_app_purchase_test.dart index a5bfdb0eb409..aa3430fbc7d2 100644 --- a/packages/in_app_purchase/integration_test/in_app_purchase_test.dart +++ b/packages/in_app_purchase/integration_test/in_app_purchase_test.dart @@ -2,6 +2,7 @@ // 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:in_app_purchase/in_app_purchase.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m index 92872d91234e..f6bdf0c4f249 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m +++ b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m @@ -2,13 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// -// FIAPReceiptManager.m -// in_app_purchase -// -// Created by Chris Yang on 3/2/19. -// - #import "FIAPReceiptManager.h" #import diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m index 872a34a94954..9b44ad766a98 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m @@ -75,7 +75,7 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar }]; [_paymentQueueHandler startObservingPaymentQueue]; _callbackChannel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_callback" + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" binaryMessenger:[registrar messenger]]; return self; } @@ -290,7 +290,7 @@ - (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { }]; } -#pragma mark - delegates +#pragma mark - delegates: - (void)handleTransactionsUpdated:(NSArray *)transactions { NSMutableArray *maps = [NSMutableArray new]; 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..a0ba91556094 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 @@ -53,10 +53,7 @@ class BillingClient { bool _enablePendingPurchases = false; /// Creates a billing client. - /// - /// The `onPurchasesUpdated` parameter must not be null. BillingClient(PurchasesUpdatedListener onPurchasesUpdated) { - assert(onPurchasesUpdated != null); channel.setMethodCallHandler(callHandler); _callbacks[kOnPurchasesUpdated] = [onPurchasesUpdated]; } @@ -74,8 +71,11 @@ class BillingClient { /// Calls /// [`BillingClient#isReady()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#isReady()) /// to get the ready status of the BillingClient instance. - Future isReady() async => - await channel.invokeMethod('BillingClient#isReady()'); + Future isReady() async { + final bool? ready = + await channel.invokeMethod('BillingClient#isReady()'); + return ready ?? false; + } /// Enable the [BillingClientWrapper] to handle pending purchases. /// @@ -100,20 +100,21 @@ class BillingClient { /// This triggers the creation of a new `BillingClient` instance in Java if /// one doesn't already exist. Future startConnection( - {@required - OnBillingServiceDisconnected onBillingServiceDisconnected}) async { + {required OnBillingServiceDisconnected + onBillingServiceDisconnected}) async { assert(_enablePendingPurchases, 'enablePendingPurchases() must be called before calling startConnection'); List disconnectCallbacks = _callbacks[_kOnBillingServiceDisconnected] ??= []; disconnectCallbacks.add(onBillingServiceDisconnected); - return BillingResultWrapper.fromJson(await channel - .invokeMapMethod( - "BillingClient#startConnection(BillingClientStateListener)", - { - 'handle': disconnectCallbacks.length - 1, - 'enablePendingPurchases': _enablePendingPurchases - })); + return BillingResultWrapper.fromJson((await channel + .invokeMapMethod( + "BillingClient#startConnection(BillingClientStateListener)", + { + 'handle': disconnectCallbacks.length - 1, + 'enablePendingPurchases': _enablePendingPurchases + })) ?? + {}); } /// Calls @@ -137,15 +138,16 @@ class BillingClient { /// `SkuDetailsParams` as direct arguments instead of requiring it constructed /// and passed in as a class. Future querySkuDetails( - {@required SkuType skuType, @required List skusList}) async { + {required SkuType skuType, required List skusList}) async { final Map arguments = { 'skuType': SkuTypeConverter().toJson(skuType), 'skusList': skusList }; - return SkuDetailsResponseWrapper.fromJson(await channel.invokeMapMethod< - String, dynamic>( - 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)', - arguments)); + return SkuDetailsResponseWrapper.fromJson((await channel.invokeMapMethod< + String, dynamic>( + 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)', + arguments)) ?? + {}); } /// Attempt to launch the Play Billing Flow for a given [skuDetails]. @@ -171,17 +173,31 @@ class BillingClient { /// skuDetails](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setskudetails) /// and [the given /// accountId](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setAccountId(java.lang.String)). + /// + /// When this method is called to purchase a subscription, an optional `oldSku` + /// can be passed in. This will tell Google Play that rather than purchasing a new subscription, + /// the user needs to upgrade/downgrade the existing subscription. + /// The [oldSku](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setoldsku) is the SKU id that the user is upgrading or downgrading from. + /// The [prorationMode](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setreplaceskusprorationmode) is the mode of proration during subscription upgrade/downgrade. + /// This value will only be effective if the `oldSku` is also set. Future launchBillingFlow( - {@required String sku, String accountId}) async { + {required String sku, + String? accountId, + String? oldSku, + ProrationMode? prorationMode}) async { assert(sku != null); final Map arguments = { 'sku': sku, 'accountId': accountId, + 'oldSku': oldSku, + 'prorationMode': ProrationModeConverter().toJson(prorationMode ?? + ProrationMode.unknownSubscriptionUpgradeDowngradePolicy) }; return BillingResultWrapper.fromJson( - await channel.invokeMapMethod( - 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)', - arguments)); + (await channel.invokeMapMethod( + 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)', + arguments)) ?? + {}); } /// Fetches recent purchases for the given [SkuType]. @@ -197,10 +213,12 @@ class BillingClient { /// skutype)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchases). Future queryPurchases(SkuType skuType) async { assert(skuType != null); - return PurchasesResultWrapper.fromJson(await channel - .invokeMapMethod( - 'BillingClient#queryPurchases(String)', - {'skuType': SkuTypeConverter().toJson(skuType)})); + return PurchasesResultWrapper.fromJson((await channel + .invokeMapMethod( + 'BillingClient#queryPurchases(String)', { + 'skuType': SkuTypeConverter().toJson(skuType) + })) ?? + {}); } /// Fetches purchase history for the given [SkuType]. @@ -218,10 +236,13 @@ class BillingClient { /// listener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchasehistoryasync). Future queryPurchaseHistory(SkuType skuType) async { assert(skuType != null); - return PurchasesHistoryResult.fromJson(await channel.invokeMapMethod( - 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)', - {'skuType': SkuTypeConverter().toJson(skuType)})); + return PurchasesHistoryResult.fromJson((await channel.invokeMapMethod< + String, dynamic>( + 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)', + { + 'skuType': SkuTypeConverter().toJson(skuType) + })) ?? + {}); } /// Consumes a given in-app product. @@ -229,20 +250,20 @@ class BillingClient { /// Consuming can only be done on an item that's owned, and as a result of consumption, the user will no longer own it. /// Consumption is done asynchronously. The method returns a Future containing a [BillingResultWrapper]. /// - /// The `purchaseToken` must not be null. /// The `developerPayload` is the developer data associated with the purchase to be consumed, it defaults to null. /// /// This wraps [`BillingClient#consumeAsync(String, ConsumeResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#consumeAsync(java.lang.String,%20com.android.billingclient.api.ConsumeResponseListener)) Future consumeAsync(String purchaseToken, - {String developerPayload}) async { + {String? developerPayload}) async { assert(purchaseToken != null); - return BillingResultWrapper.fromJson(await channel - .invokeMapMethod( - 'BillingClient#consumeAsync(String, ConsumeResponseListener)', - { - 'purchaseToken': purchaseToken, - 'developerPayload': developerPayload, - })); + return BillingResultWrapper.fromJson((await channel + .invokeMapMethod( + 'BillingClient#consumeAsync(String, ConsumeResponseListener)', + { + 'purchaseToken': purchaseToken, + 'developerPayload': developerPayload, + })) ?? + {}); } /// Acknowledge an in-app purchase. @@ -261,20 +282,20 @@ class BillingClient { /// Please refer to [acknowledge](https://developer.android.com/google/play/billing/billing_library_overview#acknowledge) for more /// details. /// - /// The `purchaseToken` must not be null. /// The `developerPayload` is the developer data associated with the purchase to be consumed, it defaults to null. /// /// This wraps [`BillingClient#acknowledgePurchase(String, AcknowledgePurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#acknowledgePurchase(com.android.billingclient.api.AcknowledgePurchaseParams,%20com.android.billingclient.api.AcknowledgePurchaseResponseListener)) Future acknowledgePurchase(String purchaseToken, - {String developerPayload}) async { + {String? developerPayload}) async { assert(purchaseToken != null); - return BillingResultWrapper.fromJson(await channel.invokeMapMethod( - 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', - { - 'purchaseToken': purchaseToken, - 'developerPayload': developerPayload, - })); + return BillingResultWrapper.fromJson((await channel.invokeMapMethod( + 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', + { + 'purchaseToken': purchaseToken, + 'developerPayload': developerPayload, + })) ?? + {}); } /// The method call handler for [channel]. @@ -283,15 +304,15 @@ class BillingClient { switch (call.method) { case kOnPurchasesUpdated: // The purchases updated listener is a singleton. - assert(_callbacks[kOnPurchasesUpdated].length == 1); + assert(_callbacks[kOnPurchasesUpdated]!.length == 1); final PurchasesUpdatedListener listener = - _callbacks[kOnPurchasesUpdated].first; + _callbacks[kOnPurchasesUpdated]!.first as PurchasesUpdatedListener; listener(PurchasesResultWrapper.fromJson( call.arguments.cast())); break; case _kOnBillingServiceDisconnected: final int handle = call.arguments['handle']; - await _callbacks[_kOnBillingServiceDisconnected][handle](); + await _callbacks[_kOnBillingServiceDisconnected]![handle](); break; } } @@ -314,6 +335,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, @@ -377,3 +403,43 @@ enum SkuType { @JsonValue('subs') subs, } + +/// Enum representing the proration mode. +/// +/// When upgrading or downgrading a subscription, set this mode to provide details +/// about the proration that will be applied when the subscription changes. +/// +/// Wraps [`BillingFlowParams.ProrationMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode) +/// See the linked documentation for an explanation of the different constants. +enum ProrationMode { +// 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. + + /// Unknown upgrade or downgrade policy. + @JsonValue(0) + unknownSubscriptionUpgradeDowngradePolicy, + + /// Replacement takes effect immediately, and the remaining time will be prorated and credited to the user. + /// + /// This is the current default behavior. + @JsonValue(1) + immediateWithTimeProration, + + /// Replacement takes effect immediately, and the billing cycle remains the same. + /// + /// The price for the remaining period will be charged. + /// This option is only available for subscription upgrade. + @JsonValue(2) + immediateAndChargeProratedPrice, + + /// Replacement takes effect immediately, and the new price will be charged on next recurrence time. + /// + /// The billing cycle stays the same. + @JsonValue(3) + immediateWithoutProration, + + /// Replacement takes effect when the old plan expires, and the new price will be charged at the same time. + @JsonValue(4) + deferred, +} diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart index 966c89176b1e..469d71b63637 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart @@ -12,40 +12,72 @@ part 'enum_converters.g.dart'; /// /// Use these in `@JsonSerializable()` classes by annotating them with /// `@BillingResponseConverter()`. -class BillingResponseConverter implements JsonConverter { +class BillingResponseConverter implements JsonConverter { /// Default const constructor. const BillingResponseConverter(); @override - BillingResponse fromJson(int json) => _$enumDecode( - _$BillingResponseEnumMap.cast(), json); + BillingResponse fromJson(int? json) { + if (json == null) { + return BillingResponse.error; + } + return _$enumDecode( + _$BillingResponseEnumMap.cast(), json); + } @override - int toJson(BillingResponse object) => _$BillingResponseEnumMap[object]; + int toJson(BillingResponse object) => _$BillingResponseEnumMap[object]!; } /// Serializer for [SkuType]. /// /// Use these in `@JsonSerializable()` classes by annotating them with /// `@SkuTypeConverter()`. -class SkuTypeConverter implements JsonConverter { +class SkuTypeConverter implements JsonConverter { /// Default const constructor. const SkuTypeConverter(); @override - SkuType fromJson(String json) => - _$enumDecode(_$SkuTypeEnumMap.cast(), json); + SkuType fromJson(String? json) { + if (json == null) { + return SkuType.inapp; + } + return _$enumDecode( + _$SkuTypeEnumMap.cast(), json); + } + + @override + String toJson(SkuType object) => _$SkuTypeEnumMap[object]!; +} + +/// Serializer for [ProrationMode]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@ProrationModeConverter()`. +class ProrationModeConverter implements JsonConverter { + /// Default const constructor. + const ProrationModeConverter(); + + @override + ProrationMode fromJson(int? json) { + if (json == null) { + return ProrationMode.unknownSubscriptionUpgradeDowngradePolicy; + } + return _$enumDecode( + _$ProrationModeEnumMap.cast(), json); + } @override - String toJson(SkuType object) => _$SkuTypeEnumMap[object]; + int toJson(ProrationMode object) => _$ProrationModeEnumMap[object]!; } // Define a class so we generate serializer helper methods for the enums @JsonSerializable() class _SerializedEnums { - BillingResponse response; - SkuType type; - PurchaseStateWrapper purchaseState; + late BillingResponse response; + late SkuType type; + late PurchaseStateWrapper purchaseState; + late ProrationMode prorationMode; } /// Serializer for [PurchaseStateWrapper]. @@ -53,18 +85,23 @@ class _SerializedEnums { /// Use these in `@JsonSerializable()` classes by annotating them with /// `@PurchaseStateConverter()`. class PurchaseStateConverter - implements JsonConverter { + implements JsonConverter { /// Default const constructor. const PurchaseStateConverter(); @override - PurchaseStateWrapper fromJson(int json) => _$enumDecode( - _$PurchaseStateWrapperEnumMap.cast(), - json); + PurchaseStateWrapper fromJson(int? json) { + if (json == null) { + return PurchaseStateWrapper.unspecified_state; + } + return _$enumDecode( + _$PurchaseStateWrapperEnumMap.cast(), + json); + } @override int toJson(PurchaseStateWrapper object) => - _$PurchaseStateWrapperEnumMap[object]; + _$PurchaseStateWrapperEnumMap[object]!; /// Converts the purchase state stored in `object` to a [PurchaseStatus]. /// @@ -78,7 +115,5 @@ class PurchaseStateConverter case PurchaseStateWrapper.unspecified_state: return PurchaseStatus.error; } - - throw ArgumentError('$object isn\'t mapped to PurchaseStatus'); } } 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..4186a2a24252 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 @@ -11,7 +11,9 @@ _SerializedEnums _$_SerializedEnumsFromJson(Map json) { ..response = _$enumDecode(_$BillingResponseEnumMap, json['response']) ..type = _$enumDecode(_$SkuTypeEnumMap, json['type']) ..purchaseState = - _$enumDecode(_$PurchaseStateWrapperEnumMap, json['purchaseState']); + _$enumDecode(_$PurchaseStateWrapperEnumMap, json['purchaseState']) + ..prorationMode = + _$enumDecode(_$ProrationModeEnumMap, json['prorationMode']); } Map _$_SerializedEnumsToJson(_SerializedEnums instance) => @@ -19,30 +21,37 @@ Map _$_SerializedEnumsToJson(_SerializedEnums instance) => 'response': _$BillingResponseEnumMap[instance.response], 'type': _$SkuTypeEnumMap[instance.type], 'purchaseState': _$PurchaseStateWrapperEnumMap[instance.purchaseState], + 'prorationMode': _$ProrationModeEnumMap[instance.prorationMode], }; -T _$enumDecode( - Map enumValues, - dynamic source, { - T unknownValue, +K _$enumDecode( + Map enumValues, + Object? source, { + K? unknownValue, }) { if (source == null) { - throw ArgumentError('A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}'); + throw ArgumentError( + 'A value must be provided. Supported values: ' + '${enumValues.values.join(', ')}', + ); } - final value = enumValues.entries - .singleWhere((e) => e.value == source, orElse: () => null) - ?.key; - - if (value == null && unknownValue == null) { - throw ArgumentError('`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}'); - } - return value ?? unknownValue; + return enumValues.entries.singleWhere( + (e) => e.value == source, + orElse: () { + if (unknownValue == null) { + throw ArgumentError( + '`$source` is not one of the supported values: ' + '${enumValues.values.join(', ')}', + ); + } + return MapEntry(unknownValue, enumValues.values.first); + }, + ).key; } const _$BillingResponseEnumMap = { + BillingResponse.serviceTimeout: -3, BillingResponse.featureNotSupported: -2, BillingResponse.serviceDisconnected: -1, BillingResponse.ok: 0, @@ -66,3 +75,11 @@ const _$PurchaseStateWrapperEnumMap = { PurchaseStateWrapper.purchased: 1, PurchaseStateWrapper.pending: 2, }; + +const _$ProrationModeEnumMap = { + ProrationMode.unknownSubscriptionUpgradeDowngradePolicy: 0, + ProrationMode.immediateWithTimeProration: 1, + ProrationMode.immediateAndChargeProratedPrice: 2, + ProrationMode.immediateWithoutProration: 3, + ProrationMode.deferred: 4, +}; diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart index 8bdd738e7ed3..05472278968a 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart @@ -27,26 +27,27 @@ class PurchaseWrapper { /// Creates a purchase wrapper with the given purchase details. @visibleForTesting PurchaseWrapper( - {@required this.orderId, - @required this.packageName, - @required this.purchaseTime, - @required this.purchaseToken, - @required this.signature, - @required this.sku, - @required this.isAutoRenewing, - @required this.originalJson, - @required this.developerPayload, - @required this.isAcknowledged, - @required this.purchaseState}); + {required this.orderId, + required this.packageName, + required this.purchaseTime, + required this.purchaseToken, + required this.signature, + required this.sku, + required this.isAutoRenewing, + required this.originalJson, + this.developerPayload, + required this.isAcknowledged, + required this.purchaseState}); /// Factory for creating a [PurchaseWrapper] from a [Map] with the purchase details. - factory PurchaseWrapper.fromJson(Map map) => _$PurchaseWrapperFromJson(map); + factory PurchaseWrapper.fromJson(Map map) => + _$PurchaseWrapperFromJson(map); @override bool operator ==(Object other) { if (identical(other, this)) return true; if (other.runtimeType != runtimeType) return false; - final PurchaseWrapper typedOther = other; + final PurchaseWrapper typedOther = other as PurchaseWrapper; return typedOther.orderId == orderId && typedOther.packageName == packageName && typedOther.purchaseTime == purchaseTime && @@ -74,22 +75,28 @@ class PurchaseWrapper { /// The unique ID for this purchase. Corresponds to the Google Payments order /// ID. + @JsonKey(defaultValue: '') final String orderId; /// The package name the purchase was made from. + @JsonKey(defaultValue: '') final String packageName; /// When the purchase was made, as an epoch timestamp. + @JsonKey(defaultValue: 0) final int purchaseTime; /// A unique ID for a given [SkuDetailsWrapper], user, and purchase. + @JsonKey(defaultValue: '') final String purchaseToken; /// Signature of purchase data, signed with the developer's private key. Uses /// RSASSA-PKCS1-v1_5. + @JsonKey(defaultValue: '') final String signature; /// The product ID of this purchase. + @JsonKey(defaultValue: '') final String sku; /// True for subscriptions that renew automatically. Does not apply to @@ -97,6 +104,8 @@ class PurchaseWrapper { /// /// For [SkuType.subs] this means that the subscription is canceled when it is /// false. + /// + /// The value is `false` for [SkuType.inapp] products. final bool isAutoRenewing; /// Details about this purchase, in JSON. @@ -105,15 +114,19 @@ class PurchaseWrapper { /// device"](https://developer.android.com/google/play/billing/billing_library_overview#Verify-purchase-device). /// Note though that verifying a purchase locally is inherently insecure (see /// the article for more details). + @JsonKey(defaultValue: '') final String originalJson; /// The payload specified by the developer when the purchase was acknowledged or consumed. - final String developerPayload; + /// + /// The value is `null` if it wasn't specified when the purchase was acknowledged or consumed. + final String? developerPayload; /// Whether the purchase has been acknowledged. /// /// A successful purchase has to be acknowledged within 3 days after the purchase via [BillingClient.acknowledgePurchase]. /// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases. + @JsonKey(defaultValue: false) final bool isAcknowledged; /// Determines the current state of the purchase. @@ -137,29 +150,33 @@ class PurchaseHistoryRecordWrapper { /// Creates a [PurchaseHistoryRecordWrapper] with the given record details. @visibleForTesting PurchaseHistoryRecordWrapper({ - @required this.purchaseTime, - @required this.purchaseToken, - @required this.signature, - @required this.sku, - @required this.originalJson, - @required this.developerPayload, + required this.purchaseTime, + required this.purchaseToken, + required this.signature, + required this.sku, + required this.originalJson, + required this.developerPayload, }); /// Factory for creating a [PurchaseHistoryRecordWrapper] from a [Map] with the record details. - factory PurchaseHistoryRecordWrapper.fromJson(Map map) => + factory PurchaseHistoryRecordWrapper.fromJson(Map map) => _$PurchaseHistoryRecordWrapperFromJson(map); /// When the purchase was made, as an epoch timestamp. + @JsonKey(defaultValue: 0) final int purchaseTime; /// A unique ID for a given [SkuDetailsWrapper], user, and purchase. + @JsonKey(defaultValue: '') final String purchaseToken; /// Signature of purchase data, signed with the developer's private key. Uses /// RSASSA-PKCS1-v1_5. + @JsonKey(defaultValue: '') final String signature; /// The product ID of this purchase. + @JsonKey(defaultValue: '') final String sku; /// Details about this purchase, in JSON. @@ -168,16 +185,20 @@ class PurchaseHistoryRecordWrapper { /// device"](https://developer.android.com/google/play/billing/billing_library_overview#Verify-purchase-device). /// Note though that verifying a purchase locally is inherently insecure (see /// the article for more details). + @JsonKey(defaultValue: '') final String originalJson; /// The payload specified by the developer when the purchase was acknowledged or consumed. - final String developerPayload; + /// + /// The value is `null` if it wasn't specified when the purchase was acknowledged or consumed. + final String? developerPayload; @override bool operator ==(Object other) { if (identical(other, this)) return true; if (other.runtimeType != runtimeType) return false; - final PurchaseHistoryRecordWrapper typedOther = other; + final PurchaseHistoryRecordWrapper typedOther = + other as PurchaseHistoryRecordWrapper; return typedOther.purchaseTime == purchaseTime && typedOther.purchaseToken == purchaseToken && typedOther.signature == signature && @@ -203,9 +224,9 @@ class PurchaseHistoryRecordWrapper { class PurchasesResultWrapper { /// Creates a [PurchasesResultWrapper] with the given purchase result details. PurchasesResultWrapper( - {@required this.responseCode, - @required this.billingResult, - @required this.purchasesList}); + {required this.responseCode, + required this.billingResult, + required this.purchasesList}); /// Factory for creating a [PurchaseResultWrapper] from a [Map] with the result details. factory PurchasesResultWrapper.fromJson(Map map) => @@ -215,7 +236,7 @@ class PurchasesResultWrapper { bool operator ==(Object other) { if (identical(other, this)) return true; if (other.runtimeType != runtimeType) return false; - final PurchasesResultWrapper typedOther = other; + final PurchasesResultWrapper typedOther = other as PurchasesResultWrapper; return typedOther.responseCode == responseCode && typedOther.purchasesList == purchasesList && typedOther.billingResult == billingResult; @@ -236,6 +257,7 @@ class PurchasesResultWrapper { /// The list of successful purchases made in this transaction. /// /// May be empty, especially if [responseCode] is not [BillingResponse.ok]. + @JsonKey(defaultValue: []) final List purchasesList; } @@ -248,7 +270,7 @@ class PurchasesResultWrapper { class PurchasesHistoryResult { /// Creates a [PurchasesHistoryResult] with the provided history. PurchasesHistoryResult( - {@required this.billingResult, @required this.purchaseHistoryRecordList}); + {required this.billingResult, required this.purchaseHistoryRecordList}); /// Factory for creating a [PurchasesHistoryResult] from a [Map] with the history result details. factory PurchasesHistoryResult.fromJson(Map map) => @@ -258,7 +280,7 @@ class PurchasesHistoryResult { bool operator ==(Object other) { if (identical(other, this)) return true; if (other.runtimeType != runtimeType) return false; - final PurchasesHistoryResult typedOther = other; + final PurchasesHistoryResult typedOther = other as PurchasesHistoryResult; return typedOther.purchaseHistoryRecordList == purchaseHistoryRecordList && typedOther.billingResult == billingResult; } @@ -272,6 +294,7 @@ class PurchasesHistoryResult { /// The list of queried purchase history records. /// /// May be empty, especially if [billingResult.responseCode] is not [BillingResponse.ok]. + @JsonKey(defaultValue: []) final List purchaseHistoryRecordList; } diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart index 3d555890b31e..5f0d936e09c2 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart @@ -8,18 +8,18 @@ part of 'purchase_wrapper.dart'; PurchaseWrapper _$PurchaseWrapperFromJson(Map json) { return PurchaseWrapper( - orderId: json['orderId'] as String, - packageName: json['packageName'] as String, - purchaseTime: json['purchaseTime'] as int, - purchaseToken: json['purchaseToken'] as String, - signature: json['signature'] as String, - sku: json['sku'] as String, + orderId: json['orderId'] as String? ?? '', + packageName: json['packageName'] as String? ?? '', + purchaseTime: json['purchaseTime'] as int? ?? 0, + purchaseToken: json['purchaseToken'] as String? ?? '', + signature: json['signature'] as String? ?? '', + sku: json['sku'] as String? ?? '', isAutoRenewing: json['isAutoRenewing'] as bool, - originalJson: json['originalJson'] as String, - developerPayload: json['developerPayload'] as String, - isAcknowledged: json['isAcknowledged'] as bool, + originalJson: json['originalJson'] as String? ?? '', + developerPayload: json['developerPayload'] as String?, + isAcknowledged: json['isAcknowledged'] as bool? ?? false, purchaseState: - const PurchaseStateConverter().fromJson(json['purchaseState'] as int), + const PurchaseStateConverter().fromJson(json['purchaseState'] as int?), ); } @@ -41,12 +41,12 @@ Map _$PurchaseWrapperToJson(PurchaseWrapper instance) => PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) { return PurchaseHistoryRecordWrapper( - purchaseTime: json['purchaseTime'] as int, - purchaseToken: json['purchaseToken'] as String, - signature: json['signature'] as String, - sku: json['sku'] as String, - originalJson: json['originalJson'] as String, - developerPayload: json['developerPayload'] as String, + purchaseTime: json['purchaseTime'] as int? ?? 0, + purchaseToken: json['purchaseToken'] as String? ?? '', + signature: json['signature'] as String? ?? '', + sku: json['sku'] as String? ?? '', + originalJson: json['originalJson'] as String? ?? '', + developerPayload: json['developerPayload'] as String?, ); } @@ -64,11 +64,16 @@ Map _$PurchaseHistoryRecordWrapperToJson( PurchasesResultWrapper _$PurchasesResultWrapperFromJson(Map json) { return PurchasesResultWrapper( responseCode: - const BillingResponseConverter().fromJson(json['responseCode'] as int), - billingResult: BillingResultWrapper.fromJson(json['billingResult'] as Map), - purchasesList: (json['purchasesList'] as List) - .map((e) => PurchaseWrapper.fromJson(e as Map)) - .toList(), + const BillingResponseConverter().fromJson(json['responseCode'] as int?), + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + purchasesList: (json['purchasesList'] as List?) + ?.map((e) => + PurchaseWrapper.fromJson(Map.from(e as Map))) + .toList() ?? + [], ); } @@ -83,10 +88,16 @@ Map _$PurchasesResultWrapperToJson( PurchasesHistoryResult _$PurchasesHistoryResultFromJson(Map json) { return PurchasesHistoryResult( - billingResult: BillingResultWrapper.fromJson(json['billingResult'] as Map), - purchaseHistoryRecordList: (json['purchaseHistoryRecordList'] as List) - .map((e) => PurchaseHistoryRecordWrapper.fromJson(e as Map)) - .toList(), + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + purchaseHistoryRecordList: + (json['purchaseHistoryRecordList'] as List?) + ?.map((e) => PurchaseHistoryRecordWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], ); } diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart index db65e2064a14..b3872958e5b9 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart @@ -13,6 +13,13 @@ import 'enum_converters.dart'; // rebuild and watch for further changes. part 'sku_details_wrapper.g.dart'; +/// The error message shown when the map represents billing result is invalid from method channel. +/// +/// This usually indicates a series underlining code issue in the plugin. +@visibleForTesting +const kInvalidBillingResultErrorMessage = + 'Invalid billing result map from method channel.'; + /// Dart wrapper around [`com.android.billingclient.api.SkuDetails`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails). /// /// Contains the details of an available product in Google Play Billing. @@ -22,22 +29,22 @@ class SkuDetailsWrapper { /// Creates a [SkuDetailsWrapper] with the given purchase details. @visibleForTesting SkuDetailsWrapper({ - @required this.description, - @required this.freeTrialPeriod, - @required this.introductoryPrice, - @required this.introductoryPriceMicros, - @required this.introductoryPriceCycles, - @required this.introductoryPricePeriod, - @required this.price, - @required this.priceAmountMicros, - @required this.priceCurrencyCode, - @required this.sku, - @required this.subscriptionPeriod, - @required this.title, - @required this.type, - @required this.isRewarded, - @required this.originalPrice, - @required this.originalPriceAmountMicros, + required this.description, + required this.freeTrialPeriod, + required this.introductoryPrice, + required this.introductoryPriceMicros, + required this.introductoryPriceCycles, + required this.introductoryPricePeriod, + required this.price, + required this.priceAmountMicros, + required this.priceCurrencyCode, + required this.sku, + required this.subscriptionPeriod, + required this.title, + required this.type, + required this.isRewarded, + required this.originalPrice, + required this.originalPriceAmountMicros, }); /// Constructs an instance of this from a key value map of data. @@ -45,55 +52,70 @@ class SkuDetailsWrapper { /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. @visibleForTesting - factory SkuDetailsWrapper.fromJson(Map map) => + factory SkuDetailsWrapper.fromJson(Map map) => _$SkuDetailsWrapperFromJson(map); /// Textual description of the product. + @JsonKey(defaultValue: '') final String description; /// Trial period in ISO 8601 format. + @JsonKey(defaultValue: '') final String freeTrialPeriod; /// Introductory price, only applies to [SkuType.subs]. Formatted ("$0.99"). + @JsonKey(defaultValue: '') final String introductoryPrice; /// [introductoryPrice] in micro-units 990000 + @JsonKey(defaultValue: '') final String introductoryPriceMicros; /// The number of billing perios that [introductoryPrice] is valid for ("2"). + @JsonKey(defaultValue: '') final String introductoryPriceCycles; /// The billing period of [introductoryPrice], in ISO 8601 format. + @JsonKey(defaultValue: '') final String introductoryPricePeriod; /// Formatted with currency symbol ("$0.99"). + @JsonKey(defaultValue: '') final String price; /// [price] in micro-units ("990000"). + @JsonKey(defaultValue: 0) final int priceAmountMicros; /// [price] ISO 4217 currency code. + @JsonKey(defaultValue: '') final String priceCurrencyCode; /// The product ID in Google Play Console. + @JsonKey(defaultValue: '') final String sku; /// Applies to [SkuType.subs], formatted in ISO 8601. + @JsonKey(defaultValue: '') final String subscriptionPeriod; /// The product's title. + @JsonKey(defaultValue: '') final String title; /// The [SkuType] of the product. final SkuType type; /// False if the product is paid. + @JsonKey(defaultValue: false) final bool isRewarded; /// The original price that the user purchased this product for. + @JsonKey(defaultValue: '') final String originalPrice; /// [originalPrice] in micro-units ("990000"). + @JsonKey(defaultValue: 0) final int originalPriceAmountMicros; @override @@ -150,7 +172,7 @@ class SkuDetailsResponseWrapper { /// Creates a [SkuDetailsResponseWrapper] with the given purchase details. @visibleForTesting SkuDetailsResponseWrapper( - {@required this.billingResult, this.skuDetailsList}); + {required this.billingResult, required this.skuDetailsList}); /// Constructs an instance of this from a key value map of data. /// @@ -163,6 +185,7 @@ class SkuDetailsResponseWrapper { final BillingResultWrapper billingResult; /// A list of [SkuDetailsWrapper] matching the query to [BillingClient.querySkuDetails]. + @JsonKey(defaultValue: []) final List skuDetailsList; @override @@ -186,22 +209,29 @@ class SkuDetailsResponseWrapper { @BillingResponseConverter() class BillingResultWrapper { /// Constructs the object with [responseCode] and [debugMessage]. - BillingResultWrapper({@required this.responseCode, this.debugMessage}); + BillingResultWrapper({required this.responseCode, this.debugMessage}); /// Constructs an instance of this from a key value map of data. /// /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. - factory BillingResultWrapper.fromJson(Map map) => - _$BillingResultWrapperFromJson(map); + factory BillingResultWrapper.fromJson(Map? map) { + if (map == null || map.isEmpty) { + return BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage); + } + return _$BillingResultWrapperFromJson(map); + } /// Response code returned in the Play Billing API calls. final BillingResponse responseCode; /// Debug message returned in the Play Billing API calls. /// + /// Defaults to `null`. /// This message uses an en-US locale and should not be shown to users. - final String debugMessage; + final String? debugMessage; @override bool operator ==(dynamic other) { diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart index 70bde9318f03..247dbd54b666 100644 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart @@ -8,22 +8,22 @@ part of 'sku_details_wrapper.dart'; SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) { return SkuDetailsWrapper( - description: json['description'] as String, - freeTrialPeriod: json['freeTrialPeriod'] as String, - introductoryPrice: json['introductoryPrice'] as String, - introductoryPriceMicros: json['introductoryPriceMicros'] as String, - introductoryPriceCycles: json['introductoryPriceCycles'] as String, - introductoryPricePeriod: json['introductoryPricePeriod'] as String, - price: json['price'] as String, - priceAmountMicros: json['priceAmountMicros'] as int, - priceCurrencyCode: json['priceCurrencyCode'] as String, - sku: json['sku'] as String, - subscriptionPeriod: json['subscriptionPeriod'] as String, - title: json['title'] as String, - type: const SkuTypeConverter().fromJson(json['type'] as String), - isRewarded: json['isRewarded'] as bool, - originalPrice: json['originalPrice'] as String, - originalPriceAmountMicros: json['originalPriceAmountMicros'] as int, + description: json['description'] as String? ?? '', + freeTrialPeriod: json['freeTrialPeriod'] as String? ?? '', + introductoryPrice: json['introductoryPrice'] as String? ?? '', + introductoryPriceMicros: json['introductoryPriceMicros'] as String? ?? '', + introductoryPriceCycles: json['introductoryPriceCycles'] as String? ?? '', + introductoryPricePeriod: json['introductoryPricePeriod'] as String? ?? '', + price: json['price'] as String? ?? '', + priceAmountMicros: json['priceAmountMicros'] as int? ?? 0, + priceCurrencyCode: json['priceCurrencyCode'] as String? ?? '', + sku: json['sku'] as String? ?? '', + subscriptionPeriod: json['subscriptionPeriod'] as String? ?? '', + title: json['title'] as String? ?? '', + type: const SkuTypeConverter().fromJson(json['type'] as String?), + isRewarded: json['isRewarded'] as bool? ?? false, + originalPrice: json['originalPrice'] as String? ?? '', + originalPriceAmountMicros: json['originalPriceAmountMicros'] as int? ?? 0, ); } @@ -49,10 +49,15 @@ Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) { return SkuDetailsResponseWrapper( - billingResult: BillingResultWrapper.fromJson(json['billingResult'] as Map), - skuDetailsList: (json['skuDetailsList'] as List) - .map((e) => SkuDetailsWrapper.fromJson(e as Map)) - .toList(), + billingResult: + BillingResultWrapper.fromJson((json['billingResult'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + skuDetailsList: (json['skuDetailsList'] as List?) + ?.map((e) => + SkuDetailsWrapper.fromJson(Map.from(e as Map))) + .toList() ?? + [], ); } @@ -66,8 +71,8 @@ Map _$SkuDetailsResponseWrapperToJson( BillingResultWrapper _$BillingResultWrapperFromJson(Map json) { return BillingResultWrapper( responseCode: - const BillingResponseConverter().fromJson(json['responseCode'] as int), - debugMessage: json['debugMessage'] as String, + const BillingResponseConverter().fromJson(json['responseCode'] as int?), + debugMessage: json['debugMessage'] as String?, ); } diff --git a/packages/in_app_purchase/lib/src/channel.dart b/packages/in_app_purchase/lib/src/channel.dart index a0b92b5d5f1e..5d140e281e7b 100644 --- a/packages/in_app_purchase/lib/src/channel.dart +++ b/packages/in_app_purchase/lib/src/channel.dart @@ -4,13 +4,6 @@ import 'package:flutter/services.dart'; -/// Method channel for the plugin's platform<-->Dart calls (all but the -/// ios->Dart calls which are carried over the [callbackChannel]). +/// Method channel for the plugin's platform<-->Dart calls. const MethodChannel channel = MethodChannel('plugins.flutter.io/in_app_purchase'); - -/// Method channel for the plugin's ios->Dart calls. -// This is in a separate channel due to historic reasons only. -// TODO(cyanglaz): Remove this. https://github.com/flutter/flutter/issues/69225 -const MethodChannel callbackChannel = - MethodChannel('plugins.flutter.io/in_app_purchase_callback'); diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart index a244ab13fc28..d4601fd809db 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart @@ -21,9 +21,9 @@ class AppStoreConnection implements InAppPurchaseConnection { /// Returns the singleton instance of the [AppStoreConnection] that should be /// used across the app. static AppStoreConnection get instance => _getOrCreateInstance(); - static AppStoreConnection _instance; - static SKPaymentQueueWrapper _skPaymentQueueWrapper; - static _TransactionObserver _observer; + static AppStoreConnection? _instance; + static late SKPaymentQueueWrapper _skPaymentQueueWrapper; + static late _TransactionObserver _observer; /// Creates an [AppStoreConnection] object. /// @@ -41,55 +41,70 @@ class AppStoreConnection implements InAppPurchaseConnection { static AppStoreConnection _getOrCreateInstance() { if (_instance != null) { - return _instance; + return _instance!; } _instance = AppStoreConnection(); _skPaymentQueueWrapper = SKPaymentQueueWrapper(); _observer = _TransactionObserver(StreamController.broadcast()); _skPaymentQueueWrapper.setTransactionObserver(observer); - return _instance; + return _instance!; } @override Future isAvailable() => SKPaymentQueueWrapper.canMakePayments(); @override - Future buyNonConsumable({@required PurchaseParam purchaseParam}) async { + Future buyNonConsumable({required PurchaseParam purchaseParam}) async { + assert( + purchaseParam.changeSubscriptionParam == null, + "`purchaseParam.changeSubscriptionParam` must be null. It is not supported on iOS " + "as Apple provides a subscription grouping mechanism. " + "Each subscription you offer must be assigned to a subscription group. " + "So the developers can group related subscriptions together to prevents users " + "from accidentally purchasing multiple subscriptions. " + "Please refer to the 'Creating a Subscription Group' sections of " + "Apple's subscription guide (https://developer.apple.com/app-store/subscriptions/)"); await _skPaymentQueueWrapper.addPayment(SKPaymentWrapper( productIdentifier: purchaseParam.productDetails.id, quantity: 1, applicationUsername: purchaseParam.applicationUserName, - simulatesAskToBuyInSandbox: purchaseParam.sandboxTesting, + simulatesAskToBuyInSandbox: purchaseParam.simulatesAskToBuyInSandbox || + // ignore: deprecated_member_use_from_same_package + purchaseParam.sandboxTesting, requestData: null)); return true; // There's no error feedback from iOS here to return. } @override Future buyConsumable( - {@required PurchaseParam purchaseParam, bool autoConsume = true}) { + {required PurchaseParam purchaseParam, bool autoConsume = true}) { assert(autoConsume == true, 'On iOS, we should always auto consume'); return buyNonConsumable(purchaseParam: purchaseParam); } @override Future completePurchase(PurchaseDetails purchase, - {String developerPayload}) async { + {String? developerPayload}) async { + if (purchase.skPaymentTransaction == null) { + throw ArgumentError( + 'completePurchase unsuccessful. The `purchase.skPaymentTransaction` is not valid'); + } await _skPaymentQueueWrapper - .finishTransaction(purchase.skPaymentTransaction); + .finishTransaction(purchase.skPaymentTransaction!); return BillingResultWrapper(responseCode: BillingResponse.ok); } @override Future consumePurchase(PurchaseDetails purchase, - {String developerPayload}) { + {String? developerPayload}) { throw UnsupportedError('consume purchase is not available on Android'); } @override Future queryPastPurchases( - {String applicationUserName}) async { - IAPError error; + {String? applicationUserName}) async { + IAPError? error; List pastPurchases = []; try { @@ -98,7 +113,6 @@ class AppStoreConnection implements InAppPurchaseConnection { await _observer.getRestoredTransactions( queue: _skPaymentQueueWrapper, applicationUserName: applicationUserName); - _observer.cleanUpRestoredTransactions(); pastPurchases = restoredTransactions.map((SKPaymentTransactionWrapper transaction) { assert(transaction.transactionState == @@ -110,16 +124,17 @@ class AppStoreConnection implements InAppPurchaseConnection { ? IAPError( source: IAPSource.AppStore, code: kPurchaseErrorCode, - message: transaction.error.domain, - details: transaction.error.userInfo, + message: transaction.error?.domain ?? '', + details: transaction.error?.userInfo, ) : null; }).toList(); + _observer.cleanUpRestoredTransactions(); } on PlatformException catch (e) { error = IAPError( source: IAPSource.AppStore, code: e.code, - message: e.message, + message: e.message ?? '', details: e.details); } on SKError catch (e) { error = IAPError( @@ -133,9 +148,12 @@ class AppStoreConnection implements InAppPurchaseConnection { } @override - Future refreshPurchaseVerificationData() async { + Future refreshPurchaseVerificationData() async { await SKRequestMaker().startRefreshReceiptRequest(); - String receipt = await SKReceiptManager.retrieveReceiptData(); + final String? receipt = await SKReceiptManager.retrieveReceiptData(); + if (receipt == null) { + return null; + } return PurchaseVerificationData( localVerificationData: receipt, serverVerificationData: receipt, @@ -152,7 +170,7 @@ class AppStoreConnection implements InAppPurchaseConnection { Set identifiers) async { final SKRequestMaker requestMaker = SKRequestMaker(); SkProductResponseWrapper response; - PlatformException exception; + PlatformException? exception; try { response = await requestMaker.startProductRequest(identifiers.toList()); } on PlatformException catch (e) { @@ -167,7 +185,7 @@ class AppStoreConnection implements InAppPurchaseConnection { ProductDetails.fromSKProduct(productWrapper)) .toList(); } - List invalidIdentifiers = response.invalidProductIdentifiers ?? []; + List invalidIdentifiers = response.invalidProductIdentifiers; if (productDetails.isEmpty) { invalidIdentifiers = identifiers.toList(); } @@ -179,7 +197,7 @@ class AppStoreConnection implements InAppPurchaseConnection { : IAPError( source: IAPSource.AppStore, code: exception.code, - message: exception.message, + message: exception.message ?? '', details: exception.details), ); return productDetailsResponse; @@ -189,27 +207,27 @@ class AppStoreConnection implements InAppPurchaseConnection { class _TransactionObserver implements SKTransactionObserverWrapper { final StreamController> purchaseUpdatedController; - Completer> _restoreCompleter; - List _restoredTransactions; - String _receiptData; + Completer>? _restoreCompleter; + List _restoredTransactions = + []; + late String _receiptData; _TransactionObserver(this.purchaseUpdatedController); Future> getRestoredTransactions( - {@required SKPaymentQueueWrapper queue, String applicationUserName}) { - assert(queue != null); + {required SKPaymentQueueWrapper queue, String? applicationUserName}) { _restoreCompleter = Completer(); queue.restoreTransactions(applicationUserName: applicationUserName); - return _restoreCompleter.future; + return _restoreCompleter!.future; } void cleanUpRestoredTransactions() { - _restoredTransactions = null; + _restoredTransactions.clear(); _restoreCompleter = null; } void updatedTransactions( - {List transactions}) async { + {required List transactions}) async { if (_restoreCompleter != null) { if (_restoredTransactions == null) { _restoredTransactions = []; @@ -233,19 +251,20 @@ class _TransactionObserver implements SKTransactionObserverWrapper { }).toList()); } - void removedTransactions({List transactions}) {} + void removedTransactions( + {required List transactions}) {} /// Triggered when there is an error while restoring transactions. - void restoreCompletedTransactionsFailed({SKError error}) { - _restoreCompleter.completeError(error); + void restoreCompletedTransactionsFailed({required SKError error}) { + _restoreCompleter!.completeError(error); } void paymentQueueRestoreCompletedTransactionsFinished() { - _restoreCompleter.complete(_restoredTransactions ?? []); + _restoreCompleter!.complete(_restoredTransactions); } bool shouldAddStorePayment( - {SKPaymentWrapper payment, SKProductWrapper product}) { + {required SKPaymentWrapper payment, required SKProductWrapper product}) { // In this unified API, we always return true to keep it consistent with the behavior on Google Play. return true; } @@ -254,7 +273,7 @@ class _TransactionObserver implements SKTransactionObserverWrapper { try { _receiptData = await SKReceiptManager.retrieveReceiptData(); } catch (e) { - _receiptData = null; + _receiptData = ''; } return _receiptData; } diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart index b980bbd77d85..1a47f3ebd095 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart @@ -8,6 +8,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; import '../../billing_client_wrappers.dart'; +import '../../in_app_purchase.dart'; import 'in_app_purchase_connection.dart'; import 'product_details.dart'; @@ -28,26 +29,27 @@ class GooglePlayConnection billingClient.enablePendingPurchases(); } _readyFuture = _connect(); - WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance!.addObserver(this); _purchaseUpdatedController = StreamController.broadcast(); ; } /// Returns the singleton instance of the [GooglePlayConnection]. static GooglePlayConnection get instance => _getOrCreateInstance(); - static GooglePlayConnection _instance; + static GooglePlayConnection? _instance; Stream> get purchaseUpdatedStream => _purchaseUpdatedController.stream; - static StreamController> _purchaseUpdatedController; + static late StreamController> + _purchaseUpdatedController; /// The [BillingClient] that's abstracted by [GooglePlayConnection]. /// /// This field should not be used out of test code. @visibleForTesting - final BillingClient billingClient; + late final BillingClient billingClient; - Future _readyFuture; + late Future _readyFuture; static Set _productIdsToConsume = Set(); @override @@ -57,17 +59,21 @@ class GooglePlayConnection } @override - Future buyNonConsumable({@required PurchaseParam purchaseParam}) async { + Future buyNonConsumable({required PurchaseParam purchaseParam}) async { BillingResultWrapper billingResultWrapper = await billingClient.launchBillingFlow( sku: purchaseParam.productDetails.id, - accountId: purchaseParam.applicationUserName); + accountId: purchaseParam.applicationUserName, + oldSku: purchaseParam + .changeSubscriptionParam?.oldPurchaseDetails.productID, + prorationMode: + purchaseParam.changeSubscriptionParam?.prorationMode); return billingResultWrapper.responseCode == BillingResponse.ok; } @override Future buyConsumable( - {@required PurchaseParam purchaseParam, bool autoConsume = true}) { + {required PurchaseParam purchaseParam, bool autoConsume = true}) { if (autoConsume) { _productIdsToConsume.add(purchaseParam.productDetails.id); } @@ -76,10 +82,14 @@ class GooglePlayConnection @override Future completePurchase(PurchaseDetails purchase, - {String developerPayload}) async { - if (purchase.billingClientPurchase.isAcknowledged) { + {String? developerPayload}) async { + if (purchase.billingClientPurchase!.isAcknowledged) { return BillingResultWrapper(responseCode: BillingResponse.ok); } + if (purchase.verificationData == null) { + throw ArgumentError( + 'completePurchase unsuccessful. The `purchase.verificationData` is not valid'); + } return await billingClient.acknowledgePurchase( purchase.verificationData.serverVerificationData, developerPayload: developerPayload); @@ -87,7 +97,11 @@ class GooglePlayConnection @override Future consumePurchase(PurchaseDetails purchase, - {String developerPayload}) { + {String? developerPayload}) { + if (purchase.verificationData == null) { + throw ArgumentError( + 'consumePurchase unsuccessful. The `purchase.verificationData` is not valid'); + } return billingClient.consumeAsync( purchase.verificationData.serverVerificationData, developerPayload: developerPayload); @@ -95,9 +109,9 @@ class GooglePlayConnection @override Future queryPastPurchases( - {String applicationUserName}) async { + {String? applicationUserName}) async { List responses; - PlatformException exception; + PlatformException? exception; try { responses = await Future.wait([ billingClient.queryPurchases(SkuType.inapp), @@ -133,7 +147,7 @@ class GooglePlayConnection .toSet(); String errorMessage = - errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : null; + errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; List pastPurchases = responses.expand((PurchasesResultWrapper response) { @@ -142,14 +156,14 @@ class GooglePlayConnection return PurchaseDetails.fromPurchase(purchaseWrapper); }).toList(); - IAPError error; + IAPError? error; if (exception != null) { error = IAPError( source: IAPSource.GooglePlay, code: exception.code, - message: exception.message, + message: exception.message ?? '', details: exception.details); - } else if (errorMessage != null) { + } else if (errorMessage.isNotEmpty) { error = IAPError( source: IAPSource.GooglePlay, code: kRestoredPurchaseErrorCode, @@ -175,11 +189,11 @@ class GooglePlayConnection static GooglePlayConnection _getOrCreateInstance() { if (_instance != null) { - return _instance; + return _instance!; } _instance = GooglePlayConnection._(); - return _instance; + return _instance!; } Future _connect() => @@ -193,7 +207,7 @@ class GooglePlayConnection Future queryProductDetails( Set identifiers) async { List responses; - PlatformException exception; + PlatformException? exception; try { responses = await Future.wait([ billingClient.querySkuDetails( @@ -235,13 +249,13 @@ class GooglePlayConnection : IAPError( source: IAPSource.GooglePlay, code: exception.code, - message: exception.message, + message: exception.message ?? '', details: exception.details)); } static Future> _getPurchaseDetailsFromResult( PurchasesResultWrapper resultWrapper) async { - IAPError error; + IAPError? error; if (resultWrapper.responseCode != BillingResponse.ok) { error = IAPError( source: IAPSource.GooglePlay, @@ -260,10 +274,13 @@ class GooglePlayConnection } else { return [ PurchaseDetails( - purchaseID: null, - productID: null, + purchaseID: '', + productID: '', transactionDate: null, - verificationData: null) + verificationData: PurchaseVerificationData( + localVerificationData: '', + serverVerificationData: '', + source: IAPSource.GooglePlay)) ..status = PurchaseStatus.error ..error = error ]; diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart index f07ff96d4403..81a0e92cc591 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart @@ -7,7 +7,6 @@ import 'dart:io'; import 'app_store_connection.dart'; import 'google_play_connection.dart'; import 'product_details.dart'; -import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/billing_client_wrappers.dart'; import './purchase_details.dart'; @@ -40,11 +39,11 @@ abstract class InAppPurchaseConnection { /// events after they start to listen. Stream> get purchaseUpdatedStream => _getStream(); - Stream> _purchaseUpdatedStream; + Stream>? _purchaseUpdatedStream; Stream> _getStream() { if (_purchaseUpdatedStream != null) { - return _purchaseUpdatedStream; + return _purchaseUpdatedStream!; } if (Platform.isAndroid) { @@ -57,7 +56,7 @@ abstract class InAppPurchaseConnection { throw UnsupportedError( 'InAppPurchase plugin only works on Android and iOS.'); } - return _purchaseUpdatedStream; + return _purchaseUpdatedStream!; } /// Whether pending purchase is enabled. @@ -133,7 +132,7 @@ abstract class InAppPurchaseConnection { /// * [queryPastPurchases], for restoring non consumable products. /// /// Calling this method for consumable items will cause unwanted behaviors! - Future buyNonConsumable({@required PurchaseParam purchaseParam}); + Future buyNonConsumable({required PurchaseParam purchaseParam}); /// Buy a consumable product. /// @@ -186,7 +185,7 @@ abstract class InAppPurchaseConnection { /// Calling this method for non consumable items will cause unwanted /// behaviors! Future buyConsumable( - {@required PurchaseParam purchaseParam, bool autoConsume = true}); + {required PurchaseParam purchaseParam, bool autoConsume = true}); /// Mark that purchased content has been delivered to the /// user. @@ -206,9 +205,9 @@ abstract class InAppPurchaseConnection { /// Warning! Failure to call this method and get a successful response within 3 days of the purchase will result a refund on Android. /// The [consumePurchase] acts as an implicit [completePurchase] on Android. /// - /// The optional parameter `developerPayload` only works on Android. + /// The optional parameter `developerPayload` (defaults to `null`) only works on Android. Future completePurchase(PurchaseDetails purchase, - {String developerPayload}); + {String? developerPayload}); /// (Play only) Mark that the user has consumed a product. /// @@ -216,16 +215,17 @@ abstract class InAppPurchaseConnection { /// delivered. The user won't be able to buy the same product again until the /// purchase of the product is consumed. /// - /// The `developerPayload` can be specified to be associated with this consumption. + /// The `developerPayload` (defaults to `null`) can be specified to be associated with this consumption. /// /// This throws an [UnsupportedError] on iOS. Future consumePurchase(PurchaseDetails purchase, - {String developerPayload}); + {String? developerPayload}); /// Query all previous purchases. /// /// The `applicationUserName` should match whatever was sent in the initial - /// `PurchaseParam`, if anything. + /// `PurchaseParam`, if anything. If no `applicationUserName` was specified in the initial + /// `PurchaseParam`, use `null`. /// /// This does not return consumed products. If you want to restore unused /// consumable products, you need to persist consumable product information @@ -236,23 +236,25 @@ abstract class InAppPurchaseConnection { /// * [refreshPurchaseVerificationData], for reloading failed /// [PurchaseDetails.verificationData]. Future queryPastPurchases( - {String applicationUserName}); + {String? applicationUserName}); /// (App Store only) retry loading purchase data after an initial failure. /// + /// If no results, a `null` value is returned. + /// /// Throws an [UnsupportedError] on Android. - Future refreshPurchaseVerificationData(); + Future refreshPurchaseVerificationData(); /// The [InAppPurchaseConnection] implemented for this platform. /// /// Throws an [UnsupportedError] when accessed on a platform other than /// Android or iOS. static InAppPurchaseConnection get instance => _getOrCreateInstance(); - static InAppPurchaseConnection _instance; + static InAppPurchaseConnection? _instance; static InAppPurchaseConnection _getOrCreateInstance() { if (_instance != null) { - return _instance; + return _instance!; } if (Platform.isAndroid) { @@ -264,7 +266,7 @@ abstract class InAppPurchaseConnection { 'InAppPurchase plugin only works on Android and iOS.'); } - return _instance; + return _instance!; } } @@ -287,9 +289,9 @@ enum IAPSource { class IAPError { /// Creates a new IAP error object with the given error details. IAPError( - {@required this.source, - @required this.code, - @required this.message, + {required this.source, + required this.code, + required this.message, this.details}); /// Which source is the error on. @@ -298,9 +300,9 @@ class IAPError { /// The error code. final String code; - /// A human-readable error message, possibly null. + /// A human-readable error message. final String message; /// Error details, possibly null. - final dynamic details; + final dynamic? details; } diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/product_details.dart b/packages/in_app_purchase/lib/src/in_app_purchase/product_details.dart index bb9e2682b9b7..a3eb79d9a450 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/product_details.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/product_details.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/store_kit_wrappers.dart'; import 'package:in_app_purchase/billing_client_wrappers.dart'; import 'in_app_purchase_connection.dart'; @@ -14,10 +13,10 @@ import 'in_app_purchase_connection.dart'; class ProductDetails { /// Creates a new product details object with the provided details. ProductDetails( - {@required this.id, - @required this.title, - @required this.description, - @required this.price, + {required this.id, + required this.title, + required this.description, + required this.price, this.skProduct, this.skuDetail}); @@ -36,13 +35,13 @@ class ProductDetails { /// Points back to the `StoreKits`'s [SKProductWrapper] object that generated this [ProductDetails] object. /// - /// This is null on Android. - final SKProductWrapper skProduct; + /// This is `null` on Android. + final SKProductWrapper? skProduct; /// Points back to the `BillingClient1`'s [SkuDetailsWrapper] object that generated this [ProductDetails] object. /// - /// This is null on iOS. - final SkuDetailsWrapper skuDetail; + /// This is `null` on iOS. + final SkuDetailsWrapper? skuDetail; /// Generate a [ProductDetails] object based on an iOS [SKProductWrapper] object. ProductDetails.fromSKProduct(SKProductWrapper product) @@ -69,7 +68,7 @@ class ProductDetails { class ProductDetailsResponse { /// Creates a new [ProductDetailsResponse] with the provided response details. ProductDetailsResponse( - {@required this.productDetails, @required this.notFoundIDs, this.error}); + {required this.productDetails, required this.notFoundIDs, this.error}); /// Each [ProductDetails] uniquely matches one valid identifier in [identifiers] of [InAppPurchaseConnection.queryProductDetails]. final List productDetails; @@ -82,7 +81,9 @@ class ProductDetailsResponse { /// A caught platform exception thrown while querying the purchases. /// + /// The value is `null` if there is no error. + /// /// It's possible for this to be null but for there still to be notFoundIds in cases where the request itself was a success but the /// requested IDs could not be found. - final IAPError error; + final IAPError? error; } diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart index 708b42c01623..b4a509055f14 100644 --- a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart +++ b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase/src/billing_client_wrappers/purchase_wrapper.dart'; import 'package:in_app_purchase/src/store_kit_wrappers/enum_converters.dart'; @@ -59,9 +58,9 @@ class PurchaseVerificationData { /// Creates a [PurchaseVerificationData] object with the provided information. PurchaseVerificationData( - {@required this.localVerificationData, - @required this.serverVerificationData, - @required this.source}); + {required this.localVerificationData, + required this.serverVerificationData, + required this.source}); } /// Status for a [PurchaseDetails]. @@ -88,9 +87,11 @@ enum PurchaseStatus { class PurchaseParam { /// Creates a new purchase parameter object with the given data. PurchaseParam( - {@required this.productDetails, + {required this.productDetails, this.applicationUserName, - this.sandboxTesting}); + this.sandboxTesting = false, + this.simulatesAskToBuyInSandbox = false, + this.changeSubscriptionParam}); /// The product to create payment for. /// @@ -103,10 +104,52 @@ class PurchaseParam { /// Do not pass in a clear text, your developer ID, the user’s Apple ID, or the /// user's Google ID for this field. /// For example, you can use a one-way hash of the user’s account name on your server. - final String applicationUserName; + final String? applicationUserName; - /// The 'sandboxTesting' is only available on iOS, set it to `true` for testing in AppStore's sandbox environment. The default value is `false`. + /// @deprecated Use [simulatesAskToBuyInSandbox] instead. + /// + /// Only available on iOS, set it to `true` to produce an "ask to buy" flow for this payment in the sandbox. + /// + /// See also [SKPaymentWrapper.simulatesAskToBuyInSandbox]. + @deprecated final bool sandboxTesting; + + /// Only available on iOS, set it to `true` to produce an "ask to buy" flow for this payment in the sandbox. + /// + /// See also [SKPaymentWrapper.simulatesAskToBuyInSandbox]. + final bool simulatesAskToBuyInSandbox; + + /// The 'changeSubscriptionParam' is only available on Android, for upgrading or + /// downgrading an existing subscription. + /// + /// This does not require on iOS since Apple provides a way to group related subscriptions + /// together in iTunesConnect. So when a subscription upgrade or downgrade is requested, + /// Apple finds the old subscription details from the group and handle it automatically. + final ChangeSubscriptionParam? changeSubscriptionParam; +} + +/// This parameter object which is only applicable on Android for upgrading or downgrading an existing subscription. +/// +/// This does not require on iOS since iTunesConnect provides a subscription grouping mechanism. +/// Each subscription you offer must be assigned to a subscription group. +/// So the developers can group related subscriptions together to prevent users from +/// accidentally purchasing multiple subscriptions. +/// +/// Please refer to the 'Creating a Subscription Group' sections of [Apple's subscription guide](https://developer.apple.com/app-store/subscriptions/) +class ChangeSubscriptionParam { + /// Creates a new change subscription param object with given data + ChangeSubscriptionParam( + {required this.oldPurchaseDetails, this.prorationMode}); + + /// The purchase object of the existing subscription that the user needs to + /// upgrade/downgrade from. + final PurchaseDetails oldPurchaseDetails; + + /// The proration mode. + /// + /// This is an optional parameter that indicates how to handle the existing + /// subscription when the new subscription comes into effect. + final ProrationMode? prorationMode; } /// Represents the transaction details of a purchase. @@ -115,7 +158,9 @@ class PurchaseParam { /// This class for simple operations. If you would like to see the detailed representation of the product, instead, use [PurchaseWrapper] on Android and [SKPaymentTransactionWrapper] on iOS. class PurchaseDetails { /// A unique identifier of the purchase. - final String purchaseID; + /// + /// The `value` is null on iOS if it is not a successful purchase. + final String? purchaseID; /// The product identifier of the purchase. final String productID; @@ -126,15 +171,16 @@ class PurchaseDetails { /// details on how to verify purchase use this data. You should never use any /// purchase data until verified. /// - /// On iOS, this may be null. Call - /// [InAppPurchaseConnection.refreshPurchaseVerificationData] to get a new + /// On iOS, [InAppPurchaseConnection.refreshPurchaseVerificationData] can be used to get a new /// [PurchaseVerificationData] object for further validation. final PurchaseVerificationData verificationData; /// The timestamp of the transaction. /// /// Milliseconds since epoch. - final String transactionDate; + /// + /// The value is `null` if [status] is not [PurchaseStatus.purchased]. + final String? transactionDate; /// The status that this [PurchaseDetails] is currently on. PurchaseStatus get status => _status; @@ -153,20 +199,22 @@ class PurchaseDetails { _status = status; } - PurchaseStatus _status; + late PurchaseStatus _status; - /// The error is only available when [status] is [PurchaseStatus.error]. - IAPError error; + /// The error details when the [status] is [PurchaseStatus.error]. + /// + /// The value is `null` if [status] is not [PurchaseStatus.error]. + IAPError? error; /// Points back to the `StoreKits`'s [SKPaymentTransactionWrapper] object that generated this [PurchaseDetails] object. /// - /// This is null on Android. - final SKPaymentTransactionWrapper skPaymentTransaction; + /// This is `null` on Android. + final SKPaymentTransactionWrapper? skPaymentTransaction; /// Points back to the `BillingClient`'s [PurchaseWrapper] object that generated this [PurchaseDetails] object. /// - /// This is null on iOS. - final PurchaseWrapper billingClientPurchase; + /// This is `null` on iOS. + final PurchaseWrapper? billingClientPurchase; /// The developer has to call [InAppPurchaseConnection.completePurchase] if the value is `true` /// and the product has been delivered to the user. @@ -179,14 +227,14 @@ class PurchaseDetails { // The platform that the object is created on. // // The value is either '_kPlatformIOS' or '_kPlatformAndroid'. - String _platform; + String? _platform; /// Creates a new PurchaseDetails object with the provided data. PurchaseDetails({ - @required this.purchaseID, - @required this.productID, - @required this.verificationData, - @required this.transactionDate, + this.purchaseID, + required this.productID, + required this.verificationData, + required this.transactionDate, this.skPaymentTransaction, this.billingClientPurchase, }); @@ -201,7 +249,7 @@ class PurchaseDetails { serverVerificationData: base64EncodedReceipt, source: IAPSource.AppStore), this.transactionDate = transaction.transactionTimeStamp != null - ? (transaction.transactionTimeStamp * 1000).toInt().toString() + ? (transaction.transactionTimeStamp! * 1000).toInt().toString() : null, this.skPaymentTransaction = transaction, this.billingClientPurchase = null, @@ -212,8 +260,8 @@ class PurchaseDetails { error = IAPError( source: IAPSource.AppStore, code: kPurchaseErrorCode, - message: transaction.error.domain, - details: transaction.error.userInfo, + message: transaction.error?.domain ?? '', + details: transaction.error?.userInfo, ); } } @@ -235,7 +283,7 @@ class PurchaseDetails { error = IAPError( source: IAPSource.GooglePlay, code: kPurchaseErrorCode, - message: null, + message: '', ); } } @@ -246,7 +294,7 @@ class PurchaseDetails { /// An instance of this class is returned in [InAppPurchaseConnection.queryPastPurchases]. class QueryPurchaseDetailsResponse { /// Creates a new [QueryPurchaseDetailsResponse] object with the provider information. - QueryPurchaseDetailsResponse({@required this.pastPurchases, this.error}); + QueryPurchaseDetailsResponse({required this.pastPurchases, this.error}); /// A list of successfully fetched past purchases. /// @@ -257,6 +305,6 @@ class QueryPurchaseDetailsResponse { /// The error when fetching past purchases. /// - /// If the fetch is successful, the value is null. - final IAPError error; + /// If the fetch is successful, the value is `null`. + final IAPError? error; } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.dart index 62188705035a..ce2c1fad406f 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.dart @@ -13,16 +13,20 @@ part 'enum_converters.g.dart'; /// Use these in `@JsonSerializable()` classes by annotating them with /// `@SKTransactionStatusConverter()`. class SKTransactionStatusConverter - implements JsonConverter { + implements JsonConverter { /// Default const constructor. const SKTransactionStatusConverter(); @override - SKPaymentTransactionStateWrapper fromJson(int json) => - _$enumDecode( - _$SKPaymentTransactionStateWrapperEnumMap - .cast(), - json); + SKPaymentTransactionStateWrapper fromJson(int? json) { + if (json == null) { + return SKPaymentTransactionStateWrapper.unspecified; + } + return _$enumDecode( + _$SKPaymentTransactionStateWrapperEnumMap + .cast(), + json); + } /// Converts an [SKPaymentTransactionStateWrapper] to a [PurchaseStatus]. PurchaseStatus toPurchaseStatus(SKPaymentTransactionStateWrapper object) { @@ -34,19 +38,70 @@ class SKTransactionStatusConverter case SKPaymentTransactionStateWrapper.restored: return PurchaseStatus.purchased; case SKPaymentTransactionStateWrapper.failed: + case SKPaymentTransactionStateWrapper.unspecified: return PurchaseStatus.error; } - - throw ArgumentError('$object isn\'t mapped to PurchaseStatus'); } @override int toJson(SKPaymentTransactionStateWrapper object) => - _$SKPaymentTransactionStateWrapperEnumMap[object]; + _$SKPaymentTransactionStateWrapperEnumMap[object]!; +} + +/// Serializer for [SKSubscriptionPeriodUnit]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SKSubscriptionPeriodUnitConverter()`. +class SKSubscriptionPeriodUnitConverter + implements JsonConverter { + /// Default const constructor. + const SKSubscriptionPeriodUnitConverter(); + + @override + SKSubscriptionPeriodUnit fromJson(int? json) { + if (json == null) { + return SKSubscriptionPeriodUnit.day; + } + return _$enumDecode( + _$SKSubscriptionPeriodUnitEnumMap + .cast(), + json); + } + + @override + int toJson(SKSubscriptionPeriodUnit object) => + _$SKSubscriptionPeriodUnitEnumMap[object]!; +} + +/// Serializer for [SKProductDiscountPaymentMode]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SKProductDiscountPaymentModeConverter()`. +class SKProductDiscountPaymentModeConverter + implements JsonConverter { + /// Default const constructor. + const SKProductDiscountPaymentModeConverter(); + + @override + SKProductDiscountPaymentMode fromJson(int? json) { + if (json == null) { + return SKProductDiscountPaymentMode.payAsYouGo; + } + return _$enumDecode( + _$SKProductDiscountPaymentModeEnumMap + .cast(), + json); + } + + @override + int toJson(SKProductDiscountPaymentMode object) => + _$SKProductDiscountPaymentModeEnumMap[object]!; } // Define a class so we generate serializer helper methods for the enums @JsonSerializable() class _SerializedEnums { - SKPaymentTransactionStateWrapper response; + late SKPaymentTransactionStateWrapper response; + late SKSubscriptionPeriodUnit unit; + late SKProductDiscountPaymentMode discountPaymentMode; } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.g.dart index f4f17df846a7..b003f435a800 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.g.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.g.dart @@ -9,33 +9,44 @@ part of 'enum_converters.dart'; _SerializedEnums _$_SerializedEnumsFromJson(Map json) { return _SerializedEnums() ..response = _$enumDecode( - _$SKPaymentTransactionStateWrapperEnumMap, json['response']); + _$SKPaymentTransactionStateWrapperEnumMap, json['response']) + ..unit = _$enumDecode(_$SKSubscriptionPeriodUnitEnumMap, json['unit']) + ..discountPaymentMode = _$enumDecode( + _$SKProductDiscountPaymentModeEnumMap, json['discountPaymentMode']); } Map _$_SerializedEnumsToJson(_SerializedEnums instance) => { 'response': _$SKPaymentTransactionStateWrapperEnumMap[instance.response], + 'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit], + 'discountPaymentMode': + _$SKProductDiscountPaymentModeEnumMap[instance.discountPaymentMode], }; -T _$enumDecode( - Map enumValues, - dynamic source, { - T unknownValue, +K _$enumDecode( + Map enumValues, + Object? source, { + K? unknownValue, }) { if (source == null) { - throw ArgumentError('A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}'); + throw ArgumentError( + 'A value must be provided. Supported values: ' + '${enumValues.values.join(', ')}', + ); } - final value = enumValues.entries - .singleWhere((e) => e.value == source, orElse: () => null) - ?.key; - - if (value == null && unknownValue == null) { - throw ArgumentError('`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}'); - } - return value ?? unknownValue; + return enumValues.entries.singleWhere( + (e) => e.value == source, + orElse: () { + if (unknownValue == null) { + throw ArgumentError( + '`$source` is not one of the supported values: ' + '${enumValues.values.join(', ')}', + ); + } + return MapEntry(unknownValue, enumValues.values.first); + }, + ).key; } const _$SKPaymentTransactionStateWrapperEnumMap = { @@ -44,4 +55,19 @@ const _$SKPaymentTransactionStateWrapperEnumMap = { SKPaymentTransactionStateWrapper.failed: 2, SKPaymentTransactionStateWrapper.restored: 3, SKPaymentTransactionStateWrapper.deferred: 4, + SKPaymentTransactionStateWrapper.unspecified: -1, +}; + +const _$SKSubscriptionPeriodUnitEnumMap = { + SKSubscriptionPeriodUnit.day: 0, + SKSubscriptionPeriodUnit.week: 1, + SKSubscriptionPeriodUnit.month: 2, + SKSubscriptionPeriodUnit.year: 3, +}; + +const _$SKProductDiscountPaymentModeEnumMap = { + SKProductDiscountPaymentMode.payAsYouGo: 0, + SKProductDiscountPaymentMode.payUpFront: 1, + SKProductDiscountPaymentMode.freeTrail: 2, + SKProductDiscountPaymentMode.unspecified: -1, }; diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index ce38759c74ec..d56fbd00c6fe 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -5,7 +5,6 @@ import 'dart:ui' show hashValues; import 'dart:async'; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/src/channel.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:flutter/services.dart'; @@ -25,7 +24,7 @@ part 'sk_payment_queue_wrapper.g.dart'; /// available at the [In-App Purchase Programming /// Guide](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html#//apple_ref/doc/uid/TP40008267). class SKPaymentQueueWrapper { - SKTransactionObserverWrapper _observer; + SKTransactionObserverWrapper? _observer; /// Returns the default payment queue. /// @@ -41,13 +40,15 @@ class SKPaymentQueueWrapper { /// Calls [`-[SKPaymentQueue transactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506026-transactions?language=objc) Future> transactions() async { - return _getTransactionList( - await channel.invokeListMethod('-[SKPaymentQueue transactions]')); + return _getTransactionList((await channel + .invokeListMethod('-[SKPaymentQueue transactions]'))!); } /// Calls [`-[SKPaymentQueue canMakePayments:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506139-canmakepayments?language=objc). static Future canMakePayments() async => - await channel.invokeMethod('-[SKPaymentQueue canMakePayments:]'); + (await channel + .invokeMethod('-[SKPaymentQueue canMakePayments:]')) ?? + false; /// Sets an observer to listen to all incoming transaction events. /// @@ -57,7 +58,7 @@ class SKPaymentQueueWrapper { /// addTransactionObserver:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506042-addtransactionobserver?language=objc). void setTransactionObserver(SKTransactionObserverWrapper observer) { _observer = observer; - callbackChannel.setMethodCallHandler(_handleObserverCallbacks); + channel.setMethodCallHandler(_handleObserverCallbacks); } /// Posts a payment to the queue. @@ -83,7 +84,7 @@ class SKPaymentQueueWrapper { Future addPayment(SKPaymentWrapper payment) async { assert(_observer != null, '[in_app_purchase]: Trying to add a payment without an observer. One must be set using `SkPaymentQueueWrapper.setTransactionObserver` before the app launches.'); - Map requestMap = payment.toMap(); + final Map requestMap = payment.toMap(); await channel.invokeMethod( '-[InAppPurchasePlugin addPayment:result:]', requestMap, @@ -103,7 +104,7 @@ class SKPaymentQueueWrapper { /// finishTransaction:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction?language=objc). Future finishTransaction( SKPaymentTransactionWrapper transaction) async { - Map requestMap = transaction.toFinishMap(); + Map requestMap = transaction.toFinishMap(); await channel.invokeMethod( '-[InAppPurchasePlugin finishTransaction:result:]', requestMap, @@ -124,28 +125,30 @@ class SKPaymentQueueWrapper { /// /// The `applicationUserName` should match the original /// [SKPaymentWrapper.applicationUsername] used in [addPayment]. + /// If no `applicationUserName` was used, `applicationUserName` should be null. /// /// This method either triggers [`-[SKPayment /// restoreCompletedTransactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506123-restorecompletedtransactions?language=objc) /// or [`-[SKPayment restoreCompletedTransactionsWithApplicationUsername:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1505992-restorecompletedtransactionswith?language=objc) /// depending on whether the `applicationUserName` is set. - Future restoreTransactions({String applicationUserName}) async { + Future restoreTransactions({String? applicationUserName}) async { await channel.invokeMethod( '-[InAppPurchasePlugin restoreTransactions:result:]', applicationUserName); } // Triage a method channel call from the platform and triggers the correct observer method. - Future _handleObserverCallbacks(MethodCall call) { + Future _handleObserverCallbacks(MethodCall call) async { assert(_observer != null, '[in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. Make sure the observer is added right at the App Launch.'); + final SKTransactionObserverWrapper observer = _observer!; switch (call.method) { case 'updatedTransactions': { final List transactions = _getTransactionList(call.arguments); return Future(() { - _observer.updatedTransactions(transactions: transactions); + observer.updatedTransactions(transactions: transactions); }); } case 'removedTransactions': @@ -153,20 +156,20 @@ class SKPaymentQueueWrapper { final List transactions = _getTransactionList(call.arguments); return Future(() { - _observer.removedTransactions(transactions: transactions); + observer.removedTransactions(transactions: transactions); }); } case 'restoreCompletedTransactionsFailed': { SKError error = SKError.fromJson(call.arguments); return Future(() { - _observer.restoreCompletedTransactionsFailed(error: error); + observer.restoreCompletedTransactionsFailed(error: error); }); } case 'paymentQueueRestoreCompletedTransactionsFinished': { return Future(() { - _observer.paymentQueueRestoreCompletedTransactionsFinished(); + observer.paymentQueueRestoreCompletedTransactionsFinished(); }); } case 'shouldAddStorePayment': @@ -176,7 +179,7 @@ class SKPaymentQueueWrapper { SKProductWrapper product = SKProductWrapper.fromJson(call.arguments['product']); return Future(() { - if (_observer.shouldAddStorePayment( + if (observer.shouldAddStorePayment( payment: payment, product: product) == true) { SKPaymentQueueWrapper().addPayment(payment); @@ -186,49 +189,52 @@ class SKPaymentQueueWrapper { default: break; } - return null; + throw PlatformException( + code: 'no_such_callback', + message: 'Did not recognize the observer callback ${call.method}.'); } // Get transaction wrapper object list from arguments. - List _getTransactionList(dynamic arguments) { - final List transactions = arguments - .map( - (dynamic map) => SKPaymentTransactionWrapper.fromJson(map)) - .toList(); - return transactions; + List _getTransactionList( + List transactionsData) { + return transactionsData.map((dynamic map) { + return SKPaymentTransactionWrapper.fromJson( + Map.castFrom(map)); + }).toList(); } } /// Dart wrapper around StoreKit's /// [NSError](https://developer.apple.com/documentation/foundation/nserror?language=objc). -@JsonSerializable(nullable: true) +@JsonSerializable() class SKError { /// Creates a new [SKError] object with the provided information. - SKError( - {@required this.code, @required this.domain, @required this.userInfo}); + SKError({required this.code, required this.domain, required this.userInfo}); /// Constructs an instance of this from a key-value map of data. /// /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. The `map` parameter must not be /// null. - factory SKError.fromJson(Map map) { - assert(map != null); + factory SKError.fromJson(Map map) { return _$SKErrorFromJson(map); } /// Error [code](https://developer.apple.com/documentation/foundation/1448136-nserror_codes) /// as defined in the Cocoa Framework. + @JsonKey(defaultValue: 0) final int code; /// Error /// [domain](https://developer.apple.com/documentation/foundation/nscocoaerrordomain?language=objc) /// as defined in the Cocoa Framework. + @JsonKey(defaultValue: '') final String domain; /// A map that contains more detailed information about the error. /// /// Any key of the map must be a valid [NSErrorUserInfoKey](https://developer.apple.com/documentation/foundation/nserroruserinfokey?language=objc). + @JsonKey(defaultValue: {}) final Map userInfo; @override @@ -239,7 +245,7 @@ class SKError { if (other.runtimeType != runtimeType) { return false; } - final SKError typedOther = other; + final SKError typedOther = other as SKError; return typedOther.code == code && typedOther.domain == domain && DeepCollectionEquality.unordered() @@ -257,11 +263,11 @@ class SKError { /// not need to create the payment object explicitly; instead, use /// [SKPaymentQueueWrapper.addPayment] directly with a product identifier to /// initiate a payment. -@JsonSerializable(nullable: true) +@JsonSerializable() class SKPaymentWrapper { /// Creates a new [SKPaymentWrapper] with the provided information. SKPaymentWrapper( - {@required this.productIdentifier, + {required this.productIdentifier, this.applicationUsername, this.requestData, this.quantity = 1, @@ -272,7 +278,7 @@ class SKPaymentWrapper { /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. The `map` parameter must not be /// null. - factory SKPaymentWrapper.fromJson(Map map) { + factory SKPaymentWrapper.fromJson(Map map) { assert(map != null); return _$SKPaymentWrapperFromJson(map); } @@ -289,6 +295,7 @@ class SKPaymentWrapper { } /// The id for the product that the payment is for. + @JsonKey(defaultValue: '') final String productIdentifier; /// An opaque id for the user's account. @@ -299,7 +306,7 @@ class SKPaymentWrapper { /// account name on your server. Don’t use the Apple ID for your developer /// account, the user’s Apple ID, or the user’s plaintext account name on /// your server. - final String applicationUsername; + final String? applicationUsername; /// Reserved for future use. /// @@ -310,18 +317,26 @@ class SKPaymentWrapper { // We also provide this property to match the iOS platform. Converted to // String from NSData from ios platform using UTF8Encoding. The / default is // null. - final String requestData; + final String? requestData; /// The amount of the product this payment is for. /// /// The default is 1. The minimum is 1. The maximum is 10. + /// + /// If the object is invalid, the value could be 0. + @JsonKey(defaultValue: 0) final int quantity; - /// Produces an "ask to buy" flow in the sandbox if set to true. Default is - /// false. + /// Produces an "ask to buy" flow in the sandbox. + /// + /// Setting it to `true` will cause a transaction to be in the state [SKPaymentTransactionStateWrapper.deferred], + /// which produce an "ask to buy" prompt that interrupts the the payment flow. + /// + /// Default is `false`. /// /// See https://developer.apple.com/in-app-purchase/ for a guide on Sandbox /// testing. + @JsonKey(defaultValue: false) final bool simulatesAskToBuyInSandbox; @override @@ -332,7 +347,7 @@ class SKPaymentWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKPaymentWrapper typedOther = other; + final SKPaymentWrapper typedOther = other as SKPaymentWrapper; return typedOther.productIdentifier == productIdentifier && typedOther.applicationUsername == applicationUsername && typedOther.quantity == quantity && diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart index 48a18e61d4d9..2b886597adc5 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart @@ -8,11 +8,12 @@ part of 'sk_payment_queue_wrapper.dart'; SKError _$SKErrorFromJson(Map json) { return SKError( - code: json['code'] as int, - domain: json['domain'] as String, - userInfo: (json['userInfo'] as Map)?.map( - (k, e) => MapEntry(k as String, e), - ), + code: json['code'] as int? ?? 0, + domain: json['domain'] as String? ?? '', + userInfo: (json['userInfo'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + ) ?? + {}, ); } @@ -24,11 +25,12 @@ Map _$SKErrorToJson(SKError instance) => { SKPaymentWrapper _$SKPaymentWrapperFromJson(Map json) { return SKPaymentWrapper( - productIdentifier: json['productIdentifier'] as String, - applicationUsername: json['applicationUsername'] as String, - requestData: json['requestData'] as String, - quantity: json['quantity'] as int, - simulatesAskToBuyInSandbox: json['simulatesAskToBuyInSandbox'] as bool, + productIdentifier: json['productIdentifier'] as String? ?? '', + applicationUsername: json['applicationUsername'] as String?, + requestData: json['requestData'] as String?, + quantity: json['quantity'] as int? ?? 0, + simulatesAskToBuyInSandbox: + json['simulatesAskToBuyInSandbox'] as bool? ?? false, ); } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart index 65f6ff8871f8..9921380e6e96 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart @@ -3,7 +3,6 @@ // found in the LICENSE file. import 'dart:ui' show hashValues; -import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; import 'sk_product_wrapper.dart'; import 'sk_payment_queue_wrapper.dart'; @@ -20,13 +19,15 @@ part 'sk_payment_transaction_wrappers.g.dart'; /// This class is a Dart wrapper around [SKTransactionObserver](https://developer.apple.com/documentation/storekit/skpaymenttransactionobserver?language=objc). abstract class SKTransactionObserverWrapper { /// Triggered when any transactions are updated. - void updatedTransactions({List transactions}); + void updatedTransactions( + {required List transactions}); /// Triggered when any transactions are removed from the payment queue. - void removedTransactions({List transactions}); + void removedTransactions( + {required List transactions}); /// Triggered when there is an error while restoring transactions. - void restoreCompletedTransactionsFailed({SKError error}); + void restoreCompletedTransactionsFailed({required SKError error}); /// Triggered when payment queue has finished sending restored transactions. void paymentQueueRestoreCompletedTransactionsFinished(); @@ -41,7 +42,7 @@ abstract class SKTransactionObserverWrapper { /// continue the transaction later by calling [addPayment] with the /// `payment` param from this method. bool shouldAddStorePayment( - {SKPaymentWrapper payment, SKProductWrapper product}); + {required SKPaymentWrapper payment, required SKProductWrapper product}); } /// The state of a transaction. @@ -85,6 +86,10 @@ enum SKPaymentTransactionStateWrapper { /// transaction to update to another state. @JsonValue(4) deferred, + + /// Indicates the transaction is in an unspecified state. + @JsonValue(-1) + unspecified, } /// Created when a payment is added to the [SKPaymentQueueWrapper]. @@ -96,16 +101,16 @@ enum SKPaymentTransactionStateWrapper { /// /// Dart wrapper around StoreKit's /// [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction?language=objc). -@JsonSerializable(nullable: true) +@JsonSerializable() class SKPaymentTransactionWrapper { /// Creates a new [SKPaymentTransactionWrapper] with the provided information. SKPaymentTransactionWrapper({ - @required this.payment, - @required this.transactionState, - @required this.originalTransaction, - @required this.transactionTimeStamp, - @required this.transactionIdentifier, - @required this.error, + required this.payment, + required this.transactionState, + this.originalTransaction, + this.transactionTimeStamp, + this.transactionIdentifier, + this.error, }); /// Constructs an instance of this from a key value map of data. @@ -113,10 +118,7 @@ class SKPaymentTransactionWrapper { /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. The `map` parameter must not be /// null. - factory SKPaymentTransactionWrapper.fromJson(Map map) { - if (map == null) { - return null; - } + factory SKPaymentTransactionWrapper.fromJson(Map map) { return _$SKPaymentTransactionWrapperFromJson(map); } @@ -130,18 +132,21 @@ class SKPaymentTransactionWrapper { /// The original Transaction. /// - /// Only available if the [transactionState] is - /// [SKPaymentTransactionStateWrapper.restored]. When the [transactionState] + /// Only available if the [transactionState] is [SKPaymentTransactionStateWrapper.restored]. + /// Otherwise the value is `null`. + /// + /// When the [transactionState] /// is [SKPaymentTransactionStateWrapper.restored], the current transaction /// object holds a new [transactionIdentifier]. - final SKPaymentTransactionWrapper originalTransaction; + final SKPaymentTransactionWrapper? originalTransaction; /// The timestamp of the transaction. /// /// Seconds since epoch. It is only defined when the [transactionState] is /// [SKPaymentTransactionStateWrapper.purchased] or /// [SKPaymentTransactionStateWrapper.restored]. - final double transactionTimeStamp; + /// Otherwise, the value is `null`. + final double? transactionTimeStamp; /// The unique string identifer of the transaction. /// @@ -150,13 +155,15 @@ class SKPaymentTransactionWrapper { /// [SKPaymentTransactionStateWrapper.restored]. You may wish to record this /// string as part of an audit trail for App Store purchases. The value of /// this string corresponds to the same property in the receipt. - final String transactionIdentifier; + /// + /// The value is `null` if it is an unsuccessful transaction. + final String? transactionIdentifier; /// The error object /// /// Only available if the [transactionState] is /// [SKPaymentTransactionStateWrapper.failed]. - final SKError error; + final SKError? error; @override bool operator ==(Object other) { @@ -166,7 +173,8 @@ class SKPaymentTransactionWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKPaymentTransactionWrapper typedOther = other; + final SKPaymentTransactionWrapper typedOther = + other as SKPaymentTransactionWrapper; return typedOther.payment == payment && typedOther.transactionState == transactionState && typedOther.originalTransaction == originalTransaction && @@ -188,8 +196,8 @@ class SKPaymentTransactionWrapper { String toString() => _$SKPaymentTransactionWrapperToJson(this).toString(); /// The payload that is used to finish this transaction. - Map toFinishMap() => { + Map toFinishMap() => { "transactionIdentifier": this.transactionIdentifier, - "productIdentifier": this.payment?.productIdentifier, + "productIdentifier": this.payment.productIdentifier, }; } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart index bc520826d9fe..4c7af21bc151 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart @@ -8,19 +8,19 @@ part of 'sk_payment_transaction_wrappers.dart'; SKPaymentTransactionWrapper _$SKPaymentTransactionWrapperFromJson(Map json) { return SKPaymentTransactionWrapper( - payment: json['payment'] == null - ? null - : SKPaymentWrapper.fromJson(json['payment'] as Map), + payment: SKPaymentWrapper.fromJson( + Map.from(json['payment'] as Map)), transactionState: const SKTransactionStatusConverter() - .fromJson(json['transactionState'] as int), + .fromJson(json['transactionState'] as int?), originalTransaction: json['originalTransaction'] == null ? null : SKPaymentTransactionWrapper.fromJson( - json['originalTransaction'] as Map), - transactionTimeStamp: (json['transactionTimeStamp'] as num)?.toDouble(), - transactionIdentifier: json['transactionIdentifier'] as String, - error: - json['error'] == null ? null : SKError.fromJson(json['error'] as Map), + Map.from(json['originalTransaction'] as Map)), + transactionTimeStamp: (json['transactionTimeStamp'] as num?)?.toDouble(), + transactionIdentifier: json['transactionIdentifier'] as String?, + error: json['error'] == null + ? null + : SKError.fromJson(Map.from(json['error'] as Map)), ); } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart index aa76971102cf..d77ea81c2d38 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -3,9 +3,9 @@ // found in the LICENSE file. import 'dart:ui' show hashValues; -import 'package:flutter/foundation.dart'; import 'package:collection/collection.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'enum_converters.dart'; // WARNING: Changes to `@JsonSerializable` classes need to be reflected in the // below generated file. Run `flutter packages pub run build_runner watch` to @@ -20,14 +20,12 @@ part 'sk_product_wrapper.g.dart'; class SkProductResponseWrapper { /// Creates an [SkProductResponseWrapper] with the given product details. SkProductResponseWrapper( - {@required this.products, @required this.invalidProductIdentifiers}); + {required this.products, required this.invalidProductIdentifiers}); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SKRequestMaker.startProductRequest]. - /// The `map` parameter must not be null. factory SkProductResponseWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); return _$SkProductResponseWrapperFromJson(map); } @@ -35,6 +33,7 @@ class SkProductResponseWrapper { /// /// One product in this list matches one valid product identifier passed to the [SKRequestMaker.startProductRequest]. /// Will be empty if the [SKRequestMaker.startProductRequest] method does not pass any correct product identifier. + @JsonKey(defaultValue: []) final List products; /// Stores product identifiers in the `productIdentifiers` from [SKRequestMaker.startProductRequest] that are not recognized by the App Store. @@ -42,6 +41,7 @@ class SkProductResponseWrapper { /// The App Store will not recognize a product identifier unless certain criteria are met. A detailed list of the criteria can be /// found here https://developer.apple.com/documentation/storekit/skproductsresponse/1505985-invalidproductidentifiers?language=objc. /// Will be empty if all the product identifiers are valid. + @JsonKey(defaultValue: []) final List invalidProductIdentifiers; @override @@ -52,7 +52,8 @@ class SkProductResponseWrapper { if (other.runtimeType != runtimeType) { return false; } - final SkProductResponseWrapper typedOther = other; + final SkProductResponseWrapper typedOther = + other as SkProductResponseWrapper; return DeepCollectionEquality().equals(typedOther.products, products) && DeepCollectionEquality().equals( typedOther.invalidProductIdentifiers, invalidProductIdentifiers); @@ -91,27 +92,32 @@ enum SKSubscriptionPeriodUnit { /// /// A period is defined by a [numberOfUnits] and a [unit], e.g for a 3 months period [numberOfUnits] is 3 and [unit] is a month. /// It is used as a property in [SKProductDiscountWrapper] and [SKProductWrapper]. -@JsonSerializable(nullable: true) +@JsonSerializable() class SKProductSubscriptionPeriodWrapper { /// Creates an [SKProductSubscriptionPeriodWrapper] for a `numberOfUnits`x`unit` period. SKProductSubscriptionPeriodWrapper( - {@required this.numberOfUnits, @required this.unit}); + {required this.numberOfUnits, required this.unit}); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SKProductDiscountWrapper.fromJson] or [SKProductWrapper.fromJson]. - /// The `map` parameter must not be null. - factory SKProductSubscriptionPeriodWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); + factory SKProductSubscriptionPeriodWrapper.fromJson( + Map? map) { + if (map == null) { + return SKProductSubscriptionPeriodWrapper( + numberOfUnits: 0, unit: SKSubscriptionPeriodUnit.day); + } return _$SKProductSubscriptionPeriodWrapperFromJson(map); } /// The number of [unit] units in this period. /// - /// Must be greater than 0. + /// Must be greater than 0 if the object is valid. + @JsonKey(defaultValue: 0) final int numberOfUnits; /// The time unit used to specify the length of this period. + @SKSubscriptionPeriodUnitConverter() final SKSubscriptionPeriodUnit unit; @override @@ -122,7 +128,8 @@ class SKProductSubscriptionPeriodWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKProductSubscriptionPeriodWrapper typedOther = other; + final SKProductSubscriptionPeriodWrapper typedOther = + other as SKProductSubscriptionPeriodWrapper; return typedOther.numberOfUnits == numberOfUnits && typedOther.unit == unit; } @@ -147,31 +154,34 @@ enum SKProductDiscountPaymentMode { /// User pays nothing during the discounted period. @JsonValue(2) freeTrail, + + /// Unspecified mode. + @JsonValue(-1) + unspecified, } /// Dart wrapper around StoreKit's [SKProductDiscount](https://developer.apple.com/documentation/storekit/skproductdiscount?language=objc). /// /// It is used as a property in [SKProductWrapper]. -@JsonSerializable(nullable: true) +@JsonSerializable() class SKProductDiscountWrapper { /// Creates an [SKProductDiscountWrapper] with the given discount details. SKProductDiscountWrapper( - {@required this.price, - @required this.priceLocale, - @required this.numberOfPeriods, - @required this.paymentMode, - @required this.subscriptionPeriod}); + {required this.price, + required this.priceLocale, + required this.numberOfPeriods, + required this.paymentMode, + required this.subscriptionPeriod}); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SKProductWrapper.fromJson]. - /// The `map` parameter must not be null. - factory SKProductDiscountWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); + factory SKProductDiscountWrapper.fromJson(Map map) { return _$SKProductDiscountWrapperFromJson(map); } /// The discounted price, in the currency that is defined in [priceLocale]. + @JsonKey(defaultValue: '') final String price; /// Includes locale information about the price, e.g. `$` as the currency symbol for US locale. @@ -179,10 +189,12 @@ class SKProductDiscountWrapper { /// The object represent the discount period length. /// - /// The value must be >= 0. + /// The value must be >= 0 if the object is valid. + @JsonKey(defaultValue: 0) final int numberOfPeriods; /// The object indicates how the discount price is charged. + @SKProductDiscountPaymentModeConverter() final SKProductDiscountPaymentMode paymentMode; /// The object represents the duration of single subscription period for the discount. @@ -199,7 +211,8 @@ class SKProductDiscountWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKProductDiscountWrapper typedOther = other; + final SKProductDiscountWrapper typedOther = + other as SKProductDiscountWrapper; return typedOther.price == price && typedOther.priceLocale == priceLocale && typedOther.numberOfPeriods == numberOfPeriods && @@ -216,40 +229,41 @@ class SKProductDiscountWrapper { /// /// A list of [SKProductWrapper] is returned in the [SKRequestMaker.startProductRequest] method, and /// should be stored for use when making a payment. -@JsonSerializable(nullable: true) +@JsonSerializable() class SKProductWrapper { /// Creates an [SKProductWrapper] with the given product details. SKProductWrapper({ - @required this.productIdentifier, - @required this.localizedTitle, - @required this.localizedDescription, - @required this.priceLocale, - @required this.subscriptionGroupIdentifier, - @required this.price, - @required this.subscriptionPeriod, - @required this.introductoryPrice, + required this.productIdentifier, + required this.localizedTitle, + required this.localizedDescription, + required this.priceLocale, + this.subscriptionGroupIdentifier, + required this.price, + this.subscriptionPeriod, + this.introductoryPrice, }); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SkProductResponseWrapper.fromJson]. - /// The `map` parameter must not be null. - factory SKProductWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); + factory SKProductWrapper.fromJson(Map map) { return _$SKProductWrapperFromJson(map); } /// The unique identifier of the product. + @JsonKey(defaultValue: '') final String productIdentifier; /// The localizedTitle of the product. /// /// It is localized based on the current locale. + @JsonKey(defaultValue: '') final String localizedTitle; /// The localized description of the product. /// /// It is localized based on the current locale. + @JsonKey(defaultValue: '') final String localizedDescription; /// Includes locale information about the price, e.g. `$` as the currency symbol for US locale. @@ -257,26 +271,29 @@ class SKProductWrapper { /// The subscription group identifier. /// + /// If the product is not a subscription, the value is `null`. + /// /// A subscription group is a collection of subscription products. /// Check [SubscriptionGroup](https://developer.apple.com/app-store/subscriptions/) for more details about subscription group. - final String subscriptionGroupIdentifier; + final String? subscriptionGroupIdentifier; /// The price of the product, in the currency that is defined in [priceLocale]. + @JsonKey(defaultValue: '') final String price; /// The object represents the subscription period of the product. /// /// Can be [null] is the product is not a subscription. - final SKProductSubscriptionPeriodWrapper subscriptionPeriod; + final SKProductSubscriptionPeriodWrapper? subscriptionPeriod; /// The object represents the duration of single subscription period. /// - /// This is only available if you set up the introductory price in the App Store Connect, otherwise it will be null. + /// This is only available if you set up the introductory price in the App Store Connect, otherwise the value is `null`. /// Programmer is also responsible to determine if the user is eligible to receive it. See https://developer.apple.com/documentation/storekit/in-app_purchase/offering_introductory_pricing_in_your_app?language=objc /// for more details. /// The [subscriptionPeriod] of the discount is independent of the product's [subscriptionPeriod], /// and their units and duration do not have to be matched. - final SKProductDiscountWrapper introductoryPrice; + final SKProductDiscountWrapper? introductoryPrice; @override bool operator ==(Object other) { @@ -286,7 +303,7 @@ class SKProductWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKProductWrapper typedOther = other; + final SKProductWrapper typedOther = other as SKProductWrapper; return typedOther.productIdentifier == productIdentifier && typedOther.localizedTitle == localizedTitle && typedOther.localizedDescription == localizedDescription && @@ -319,21 +336,24 @@ class SKProductWrapper { class SKPriceLocaleWrapper { /// Creates a new price locale for `currencySymbol` and `currencyCode`. SKPriceLocaleWrapper( - {@required this.currencySymbol, @required this.currencyCode}); + {required this.currencySymbol, required this.currencyCode}); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SKProductWrapper.fromJson] and [SKProductDiscountWrapper.fromJson]. - /// The `map` parameter must not be null. - factory SKPriceLocaleWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); + factory SKPriceLocaleWrapper.fromJson(Map? map) { + if (map == null) { + return SKPriceLocaleWrapper(currencyCode: '', currencySymbol: ''); + } return _$SKPriceLocaleWrapperFromJson(map); } ///The currency symbol for the locale, e.g. $ for US locale. + @JsonKey(defaultValue: '') final String currencySymbol; ///The currency code for the locale, e.g. USD for US locale. + @JsonKey(defaultValue: '') final String currencyCode; @override @@ -344,7 +364,7 @@ class SKPriceLocaleWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKPriceLocaleWrapper typedOther = other; + final SKPriceLocaleWrapper typedOther = other as SKPriceLocaleWrapper; return typedOther.currencySymbol == currencySymbol && typedOther.currencyCode == currencyCode; } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart index cf27852263ba..8c2eed3d6070 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -8,12 +8,16 @@ part of 'sk_product_wrapper.dart'; SkProductResponseWrapper _$SkProductResponseWrapperFromJson(Map json) { return SkProductResponseWrapper( - products: (json['products'] as List) - .map((e) => SKProductWrapper.fromJson(e as Map)) - .toList(), - invalidProductIdentifiers: (json['invalidProductIdentifiers'] as List) - .map((e) => e as String) - .toList(), + products: (json['products'] as List?) + ?.map((e) => + SKProductWrapper.fromJson(Map.from(e as Map))) + .toList() ?? + [], + invalidProductIdentifiers: + (json['invalidProductIdentifiers'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], ); } @@ -27,8 +31,9 @@ Map _$SkProductResponseWrapperToJson( SKProductSubscriptionPeriodWrapper _$SKProductSubscriptionPeriodWrapperFromJson( Map json) { return SKProductSubscriptionPeriodWrapper( - numberOfUnits: json['numberOfUnits'] as int, - unit: _$enumDecodeNullable(_$SKSubscriptionPeriodUnitEnumMap, json['unit']), + numberOfUnits: json['numberOfUnits'] as int? ?? 0, + unit: const SKSubscriptionPeriodUnitConverter() + .fromJson(json['unit'] as int?), ); } @@ -36,61 +41,23 @@ Map _$SKProductSubscriptionPeriodWrapperToJson( SKProductSubscriptionPeriodWrapper instance) => { 'numberOfUnits': instance.numberOfUnits, - 'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit], + 'unit': const SKSubscriptionPeriodUnitConverter().toJson(instance.unit), }; -T _$enumDecode( - Map enumValues, - dynamic source, { - T unknownValue, -}) { - if (source == null) { - throw ArgumentError('A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}'); - } - - final value = enumValues.entries - .singleWhere((e) => e.value == source, orElse: () => null) - ?.key; - - if (value == null && unknownValue == null) { - throw ArgumentError('`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}'); - } - return value ?? unknownValue; -} - -T _$enumDecodeNullable( - Map enumValues, - dynamic source, { - T unknownValue, -}) { - if (source == null) { - return null; - } - return _$enumDecode(enumValues, source, unknownValue: unknownValue); -} - -const _$SKSubscriptionPeriodUnitEnumMap = { - SKSubscriptionPeriodUnit.day: 0, - SKSubscriptionPeriodUnit.week: 1, - SKSubscriptionPeriodUnit.month: 2, - SKSubscriptionPeriodUnit.year: 3, -}; - SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) { return SKProductDiscountWrapper( - price: json['price'] as String, - priceLocale: json['priceLocale'] == null - ? null - : SKPriceLocaleWrapper.fromJson(json['priceLocale'] as Map), - numberOfPeriods: json['numberOfPeriods'] as int, - paymentMode: _$enumDecodeNullable( - _$SKProductDiscountPaymentModeEnumMap, json['paymentMode']), - subscriptionPeriod: json['subscriptionPeriod'] == null - ? null - : SKProductSubscriptionPeriodWrapper.fromJson( - json['subscriptionPeriod'] as Map), + price: json['price'] as String? ?? '', + priceLocale: + SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + numberOfPeriods: json['numberOfPeriods'] as int? ?? 0, + paymentMode: const SKProductDiscountPaymentModeConverter() + .fromJson(json['paymentMode'] as int?), + subscriptionPeriod: SKProductSubscriptionPeriodWrapper.fromJson( + (json['subscriptionPeriod'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), ); } @@ -100,34 +67,32 @@ Map _$SKProductDiscountWrapperToJson( 'price': instance.price, 'priceLocale': instance.priceLocale, 'numberOfPeriods': instance.numberOfPeriods, - 'paymentMode': - _$SKProductDiscountPaymentModeEnumMap[instance.paymentMode], + 'paymentMode': const SKProductDiscountPaymentModeConverter() + .toJson(instance.paymentMode), 'subscriptionPeriod': instance.subscriptionPeriod, }; -const _$SKProductDiscountPaymentModeEnumMap = { - SKProductDiscountPaymentMode.payAsYouGo: 0, - SKProductDiscountPaymentMode.payUpFront: 1, - SKProductDiscountPaymentMode.freeTrail: 2, -}; - SKProductWrapper _$SKProductWrapperFromJson(Map json) { return SKProductWrapper( - productIdentifier: json['productIdentifier'] as String, - localizedTitle: json['localizedTitle'] as String, - localizedDescription: json['localizedDescription'] as String, - priceLocale: json['priceLocale'] == null - ? null - : SKPriceLocaleWrapper.fromJson(json['priceLocale'] as Map), - subscriptionGroupIdentifier: json['subscriptionGroupIdentifier'] as String, - price: json['price'] as String, + productIdentifier: json['productIdentifier'] as String? ?? '', + localizedTitle: json['localizedTitle'] as String? ?? '', + localizedDescription: json['localizedDescription'] as String? ?? '', + priceLocale: + SKPriceLocaleWrapper.fromJson((json['priceLocale'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), + subscriptionGroupIdentifier: json['subscriptionGroupIdentifier'] as String?, + price: json['price'] as String? ?? '', subscriptionPeriod: json['subscriptionPeriod'] == null ? null : SKProductSubscriptionPeriodWrapper.fromJson( - json['subscriptionPeriod'] as Map), + (json['subscriptionPeriod'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + )), introductoryPrice: json['introductoryPrice'] == null ? null - : SKProductDiscountWrapper.fromJson(json['introductoryPrice'] as Map), + : SKProductDiscountWrapper.fromJson( + Map.from(json['introductoryPrice'] as Map)), ); } @@ -145,8 +110,8 @@ Map _$SKProductWrapperToJson(SKProductWrapper instance) => SKPriceLocaleWrapper _$SKPriceLocaleWrapperFromJson(Map json) { return SKPriceLocaleWrapper( - currencySymbol: json['currencySymbol'] as String, - currencyCode: json['currencyCode'] as String, + currencySymbol: json['currencySymbol'] as String? ?? '', + currencyCode: json['currencyCode'] as String? ?? '', ); } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart index 85af9dedc7c3..16bcb77a2c70 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart @@ -14,8 +14,9 @@ class SKReceiptManager { /// There are 2 ways to do so. Either validate locally or validate with App Store. /// For more details on how to validate the receipt data, you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). /// If the receipt is invalid or missing, you can use [SKRequestMaker.startRefreshReceiptRequest] to request a new receipt. - static Future retrieveReceiptData() { - return channel.invokeMethod( - '-[InAppPurchasePlugin retrieveReceiptData:result:]'); + static Future retrieveReceiptData() async { + return (await channel.invokeMethod( + '-[InAppPurchasePlugin retrieveReceiptData:result:]')) ?? + ''; } } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart index 959113cd66d8..c22df0a9dbdd 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart +++ b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart @@ -24,7 +24,7 @@ class SKRequestMaker { /// A [PlatformException] is thrown if the platform code making the request fails. Future startProductRequest( List productIdentifiers) async { - final Map productResponseMap = + final Map? productResponseMap = await channel.invokeMapMethod( '-[InAppPurchasePlugin startProductRequest:result:]', productIdentifiers, @@ -47,7 +47,8 @@ class SKRequestMaker { /// * isExpired: whether the receipt is expired. /// * isRevoked: whether the receipt has been revoked. /// * isVolumePurchase: whether the receipt is a Volume Purchase Plan receipt. - Future startRefreshReceiptRequest({Map receiptProperties}) { + Future startRefreshReceiptRequest( + {Map? receiptProperties}) { return channel.invokeMethod( '-[InAppPurchasePlugin refreshReceipt:result:]', receiptProperties, diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml index eda865e487f7..6175e8cbf1dc 100644 --- a/packages/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/pubspec.yaml @@ -1,30 +1,26 @@ 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.4.1 dependencies: - async: ^2.0.8 - collection: ^1.14.11 flutter: sdk: flutter - json_annotation: ^3.0.0 - meta: ^1.1.6 + json_annotation: ^4.0.0 + meta: ^1.3.0 + collection: ^1.15.0 dev_dependencies: - build_runner: ^1.0.0 - json_serializable: ^3.2.0 + build_runner: ^1.11.1 + json_serializable: ^4.0.0 flutter_test: sdk: flutter flutter_driver: sdk: flutter - in_app_purchase_example: - path: example/ - test: ^1.5.2 - shared_preferences: ^0.5.2 + test: ^1.16.0 integration_test: path: ../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: plugin: @@ -36,5 +32,5 @@ flutter: pluginClass: InAppPurchasePlugin environment: - sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.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..3aa62ddd96a1 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 @@ -16,7 +16,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); - BillingClient billingClient; + late BillingClient billingClient; setUpAll(() => channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler)); @@ -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)'; @@ -78,6 +96,20 @@ void main() { equals( {'handle': 0, 'enablePendingPurchases': true})); }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: methodName, + value: null, + ); + + expect( + await billingClient.startConnection( + onBillingServiceDisconnected: () {}), + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + }); }); test('endConnection', () async { @@ -133,6 +165,20 @@ void main() { expect(response.billingResult, equals(billingResult)); expect(response.skuDetailsList, contains(dummySkuDetails)); }); + + test('handles null method channel response', () async { + stubPlatform.addResponse(name: queryMethodName, value: null); + + final SkuDetailsResponseWrapper response = await billingClient + .querySkuDetails( + skuType: SkuType.inapp, skusList: ['invalid']); + + BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage); + expect(response.billingResult, equals(billingResult)); + expect(response.skuDetailsList, isEmpty); + }); }); group('launchBillingFlow', () { @@ -161,6 +207,64 @@ void main() { expect(arguments['accountId'], equals(accountId)); }); + test( + 'serializes and deserializes data on change subscription without proration', + () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = "hashedAccountId"; + + expect( + await billingClient.launchBillingFlow( + sku: skuDetails.sku, + accountId: accountId, + oldSku: dummyOldPurchase.sku), + equals(expectedBillingResult)); + Map arguments = + stubPlatform.previousCallMatching(launchMethodName).arguments; + expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['accountId'], equals(accountId)); + expect(arguments['oldSku'], equals(dummyOldPurchase.sku)); + }); + + test( + 'serializes and deserializes data on change subscription with proration', + () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = "hashedAccountId"; + final prorationMode = ProrationMode.immediateAndChargeProratedPrice; + + expect( + await billingClient.launchBillingFlow( + sku: skuDetails.sku, + accountId: accountId, + oldSku: dummyOldPurchase.sku, + prorationMode: prorationMode), + equals(expectedBillingResult)); + Map arguments = + stubPlatform.previousCallMatching(launchMethodName).arguments; + expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['accountId'], equals(accountId)); + expect(arguments['oldSku'], equals(dummyOldPurchase.sku)); + expect(arguments['prorationMode'], + ProrationModeConverter().toJson(prorationMode)); + }); + test('handles null accountId', () async { const String debugMessage = 'dummy message'; final BillingResponse responseCode = BillingResponse.ok; @@ -179,6 +283,19 @@ void main() { expect(arguments['sku'], equals(skuDetails.sku)); expect(arguments['accountId'], isNull); }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: launchMethodName, + value: null, + ); + final SkuDetailsWrapper skuDetails = dummySkuDetails; + expect( + await billingClient.launchBillingFlow(sku: skuDetails.sku), + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + }); }); group('queryPurchases', () { @@ -210,10 +327,6 @@ void main() { expect(response.purchasesList, equals(expectedList)); }); - test('checks for null params', () async { - expect(() => billingClient.queryPurchases(null), throwsAssertionError); - }); - test('handles empty purchases', () async { final BillingResponse expectedCode = BillingResponse.userCanceled; const String debugMessage = 'dummy message'; @@ -233,6 +346,23 @@ void main() { expect(response.responseCode, equals(expectedCode)); expect(response.purchasesList, isEmpty); }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: queryPurchasesMethodName, + value: null, + ); + final PurchasesResultWrapper response = + await billingClient.queryPurchases(SkuType.inapp); + + expect( + response.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(response.responseCode, BillingResponse.error); + expect(response.purchasesList, isEmpty); + }); }); group('queryPurchaseHistory', () { @@ -264,11 +394,6 @@ void main() { expect(response.purchaseHistoryRecordList, equals(expectedList)); }); - test('checks for null params', () async { - expect( - () => billingClient.queryPurchaseHistory(null), throwsAssertionError); - }); - test('handles empty purchases', () async { final BillingResponse expectedCode = BillingResponse.userCanceled; const String debugMessage = 'dummy message'; @@ -285,6 +410,22 @@ void main() { expect(response.billingResult, equals(expectedBillingResult)); expect(response.purchaseHistoryRecordList, isEmpty); }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: queryPurchaseHistoryMethodName, + value: null, + ); + final PurchasesHistoryResult response = + await billingClient.queryPurchaseHistory(SkuType.inapp); + + expect( + response.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(response.purchaseHistoryRecordList, isEmpty); + }); }); group('consume purchases', () { @@ -304,6 +445,21 @@ void main() { expect(billingResult, equals(expectedBillingResult)); }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: consumeMethodName, + value: null, + ); + final BillingResultWrapper billingResult = await billingClient + .consumeAsync('dummy token', developerPayload: 'dummy payload'); + + expect( + billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + }); }); group('acknowledge purchases', () { @@ -324,5 +480,20 @@ void main() { expect(billingResult, equals(expectedBillingResult)); }); + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: acknowledgeMethodName, + value: null, + ); + final BillingResultWrapper billingResult = + await billingClient.acknowledgePurchase('dummy token', + developerPayload: 'dummy payload'); + + expect( + billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + }); }); } diff --git a/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart index 978252a3d118..df5b8f5bde22 100644 --- a/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart +++ b/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -46,6 +46,20 @@ final PurchaseHistoryRecordWrapper dummyPurchaseHistoryRecord = developerPayload: 'dummy payload', ); +final PurchaseWrapper dummyOldPurchase = PurchaseWrapper( + orderId: 'oldOrderId', + packageName: 'oldPackageName', + purchaseTime: 0, + signature: 'oldSignature', + sku: 'oldSku', + purchaseToken: 'oldPurchaseToken', + isAutoRenewing: false, + originalJson: '', + developerPayload: 'old dummy payload', + isAcknowledged: true, + purchaseState: PurchaseStateWrapper.purchased, +); + void main() { group('PurchaseWrapper', () { test('converts from map', () { @@ -62,6 +76,7 @@ void main() { expect(details.purchaseID, dummyPurchase.orderId); expect(details.productID, dummyPurchase.sku); expect(details.transactionDate, dummyPurchase.purchaseTime.toString()); + expect(details.verificationData, isNotNull); expect(details.verificationData.source, IAPSource.GooglePlay); expect(details.verificationData.localVerificationData, dummyPurchase.originalJson); @@ -111,6 +126,18 @@ void main() { expect(parsed.responseCode, equals(expected.responseCode)); expect(parsed.purchasesList, containsAll(expected.purchasesList)); }); + + test('parsed from empty map', () { + final PurchasesResultWrapper parsed = + PurchasesResultWrapper.fromJson({}); + expect( + parsed.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(parsed.responseCode, BillingResponse.error); + expect(parsed.purchasesList, isEmpty); + }); }); group('PurchasesHistoryResult', () { @@ -139,6 +166,17 @@ void main() { expect(parsed.purchaseHistoryRecordList, containsAll(expected.purchaseHistoryRecordList)); }); + + test('parsed from empty map', () { + final PurchasesHistoryResult parsed = + PurchasesHistoryResult.fromJson({}); + expect( + parsed.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(parsed.purchaseHistoryRecordList, isEmpty); + }); }); } diff --git a/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart index c305e6df88cc..13715eeb9fc0 100644 --- a/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart +++ b/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart @@ -99,6 +99,33 @@ void main() { expect(parsed.billingResult, equals(expected.billingResult)); expect(parsed.skuDetailsList, containsAll(expected.skuDetailsList)); }); + + test('fromJson creates an object with default values', () { + final SkuDetailsResponseWrapper skuDetails = + SkuDetailsResponseWrapper.fromJson({}); + expect( + skuDetails.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(skuDetails.skuDetailsList, isEmpty); + }); + }); + + group('BillingResultWrapper', () { + test('fromJson on empty map creates an object with default values', () { + final BillingResultWrapper billingResult = + BillingResultWrapper.fromJson({}); + expect(billingResult.debugMessage, kInvalidBillingResultErrorMessage); + expect(billingResult.responseCode, BillingResponse.error); + }); + + test('fromJson on null creates an object with default values', () { + final BillingResultWrapper billingResult = + BillingResultWrapper.fromJson(null); + expect(billingResult.debugMessage, kInvalidBillingResultErrorMessage); + expect(billingResult.responseCode, BillingResponse.error); + }); }); } diff --git a/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart b/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart index b22737ca041b..bfcab085e26a 100644 --- a/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart +++ b/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart @@ -15,6 +15,7 @@ import 'package:in_app_purchase/src/in_app_purchase/app_store_connection.dart'; import 'package:in_app_purchase/src/in_app_purchase/in_app_purchase_connection.dart'; import 'package:in_app_purchase/src/in_app_purchase/product_details.dart'; import 'package:in_app_purchase/store_kit_wrappers.dart'; +import '../billing_client_wrappers/purchase_wrapper_test.dart'; import '../store_kit_wrappers/sk_test_stub_objects.dart'; void main() { @@ -61,10 +62,11 @@ void main() { .queryProductDetails(['123', '456', '789'].toSet()); expect(response.productDetails, []); expect(response.notFoundIDs, ['123', '456', '789']); - expect(response.error.source, IAPSource.AppStore); - expect(response.error.code, 'error_code'); - expect(response.error.message, 'error_message'); - expect(response.error.details, {'info': 'error_info'}); + expect(response.error, isNotNull); + expect(response.error!.source, IAPSource.AppStore); + expect(response.error!.code, 'error_code'); + expect(response.error!.message, 'error_message'); + expect(response.error!.details, {'info': 'error_info'}); }); }); @@ -81,6 +83,8 @@ void main() { fakeIOSPlatform.transactions.first.transactionIdentifier); expect(response.pastPurchases.last.purchaseID, fakeIOSPlatform.transactions.last.transactionIdentifier); + expect(response.pastPurchases, isNotEmpty); + expect(response.pastPurchases.first.verificationData, isNotNull); expect( response.pastPurchases.first.verificationData.localVerificationData, 'dummy base64data'); @@ -97,7 +101,7 @@ void main() { Stream> stream = AppStoreConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { completer.complete(purchaseDetailsList); @@ -130,9 +134,10 @@ void main() { QueryPurchaseDetailsResponse response = await AppStoreConnection.instance.queryPastPurchases(); expect(response.pastPurchases, isEmpty); - expect(response.error.source, IAPSource.AppStore); - expect(response.error.message, 'error_test'); - expect(response.error.details, {'message': 'errorMessage'}); + expect(response.error, isNotNull); + expect(response.error!.source, IAPSource.AppStore); + expect(response.error!.message, 'error_test'); + expect(response.error!.details, {'message': 'errorMessage'}); }); test('receipt error should populate null to verificationData.data', @@ -142,18 +147,19 @@ void main() { await AppStoreConnection.instance.queryPastPurchases(); expect( response.pastPurchases.first.verificationData.localVerificationData, - null); + isEmpty); expect( response.pastPurchases.first.verificationData.serverVerificationData, - null); + isEmpty); }); }); group('refresh receipt data', () { test('should refresh receipt data', () async { - PurchaseVerificationData receiptData = + PurchaseVerificationData? receiptData = await AppStoreConnection.instance.refreshPurchaseVerificationData(); - expect(receiptData.source, IAPSource.AppStore); + expect(receiptData, isNotNull); + expect(receiptData!.source, IAPSource.AppStore); expect(receiptData.localVerificationData, 'refreshed receipt data'); expect(receiptData.serverVerificationData, 'refreshed receipt data'); }); @@ -168,7 +174,7 @@ void main() { Stream> stream = AppStoreConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { details.addAll(purchaseDetailsList); if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { @@ -195,7 +201,7 @@ void main() { Stream> stream = AppStoreConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { details.addAll(purchaseDetailsList); if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { @@ -228,16 +234,16 @@ void main() { fakeIOSPlatform.testTransactionFail = true; List details = []; Completer completer = Completer(); - IAPError error; + late IAPError error; Stream> stream = AppStoreConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { details.addAll(purchaseDetailsList); purchaseDetailsList.forEach((purchaseDetails) { if (purchaseDetails.status == PurchaseStatus.error) { - error = purchaseDetails.error; + error = purchaseDetails.error!; completer.complete(error); subscription.cancel(); } @@ -263,7 +269,7 @@ void main() { Completer completer = Completer(); Stream> stream = AppStoreConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = stream.listen((purchaseDetailsList) { details.addAll(purchaseDetailsList); purchaseDetailsList.forEach((purchaseDetails) { @@ -288,7 +294,9 @@ void main() { group('consume purchase', () { test('should throw when calling consume purchase on iOS', () async { - expect(() => AppStoreConnection.instance.consumePurchase(null), + expect( + () => AppStoreConnection.instance + .consumePurchase(PurchaseDetails.fromPurchase(dummyPurchase)), throwsUnsupportedError); }); }); @@ -300,16 +308,16 @@ class FakeIOSPlatform { } // pre-configured store informations - String receiptData; - Set validProductIDs; - Map validProducts; - List transactions; - List finishedTransactions; - bool testRestoredTransactionsNull; - bool testTransactionFail; - PlatformException queryProductException; - PlatformException restoreException; - SKError testRestoredError; + String? receiptData; + late Set validProductIDs; + late Map validProducts; + late List transactions; + late List finishedTransactions; + late bool testRestoredTransactionsNull; + late bool testTransactionFail; + PlatformException? queryProductException; + PlatformException? restoreException; + SKError? testRestoredError; void reset() { transactions = []; @@ -317,7 +325,8 @@ class FakeIOSPlatform { validProductIDs = ['123', '456'].toSet(); validProducts = Map(); for (String validID in validProductIDs) { - Map productWrapperMap = buildProductMap(dummyProductWrapper); + Map productWrapperMap = + buildProductMap(dummyProductWrapper); productWrapperMap['productIdentifier'] = validID; validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap); } @@ -350,7 +359,7 @@ class FakeIOSPlatform { SKPaymentTransactionWrapper createPendingTransaction(String id) { return SKPaymentTransactionWrapper( - transactionIdentifier: null, + transactionIdentifier: '', payment: SKPaymentWrapper(productIdentifier: id), transactionState: SKPaymentTransactionStateWrapper.purchasing, transactionTimeStamp: 123123.121, @@ -371,7 +380,7 @@ class FakeIOSPlatform { SKPaymentTransactionWrapper createFailedTransaction(String productId) { return SKPaymentTransactionWrapper( - transactionIdentifier: null, + transactionIdentifier: '', payment: SKPaymentWrapper(productIdentifier: productId), transactionState: SKPaymentTransactionStateWrapper.failed, transactionTimeStamp: 123123.121, @@ -388,7 +397,7 @@ class FakeIOSPlatform { return Future.value(true); case '-[InAppPurchasePlugin startProductRequest:result:]': if (queryProductException != null) { - throw queryProductException; + throw queryProductException!; } List productIDS = List.castFrom(call.arguments); @@ -399,7 +408,7 @@ class FakeIOSPlatform { if (!validProductIDs.contains(productID)) { invalidFound.add(productID); } else { - products.add(validProducts[productID]); + products.add(validProducts[productID]!); } } SkProductResponseWrapper response = SkProductResponseWrapper( @@ -408,11 +417,11 @@ class FakeIOSPlatform { buildProductResponseMap(response)); case '-[InAppPurchasePlugin restoreTransactions:result:]': if (restoreException != null) { - throw restoreException; + throw restoreException!; } if (testRestoredError != null) { AppStoreConnection.observer - .restoreCompletedTransactionsFailed(error: testRestoredError); + .restoreCompletedTransactionsFailed(error: testRestoredError!); return Future.sync(() {}); } if (!testRestoredTransactionsNull) { @@ -428,7 +437,6 @@ class FakeIOSPlatform { } else { throw PlatformException(code: 'no_receipt_data'); } - break; case '-[InAppPurchasePlugin refreshReceipt:result:]': receiptData = 'refreshed receipt data'; return Future.sync(() {}); @@ -445,7 +453,8 @@ class FakeIOSPlatform { .updatedTransactions(transactions: [transaction_failed]); } else { SKPaymentTransactionWrapper transaction_finished = - createPurchasedTransaction(id, transaction.transactionIdentifier); + createPurchasedTransaction( + id, transaction.transactionIdentifier ?? ''); AppStoreConnection.observer .updatedTransactions(transactions: [transaction_finished]); } diff --git a/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart b/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart index f06c4ff7efef..79c2ee436c5c 100644 --- a/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart +++ b/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart @@ -9,7 +9,7 @@ import 'package:flutter_test/flutter_test.dart' show TestWidgetsFlutterBinding; import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; import 'package:test/test.dart'; -import 'package:flutter/widgets.dart' hide TypeMatcher; +import 'package:flutter/widgets.dart' as widgets; import 'package:in_app_purchase/billing_client_wrappers.dart'; import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; import 'package:in_app_purchase/src/in_app_purchase/google_play_connection.dart'; @@ -24,7 +24,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); - GooglePlayConnection connection; + late GooglePlayConnection connection; const String startConnectionCall = 'BillingClient#startConnection(BillingClientStateListener)'; const String endConnectionCall = 'BillingClient#endConnection()'; @@ -34,7 +34,7 @@ void main() { }); setUp(() { - WidgetsFlutterBinding.ensureInitialized(); + widgets.WidgetsFlutterBinding.ensureInitialized(); const String debugMessage = 'dummy message'; final BillingResponse responseCode = BillingResponse.ok; final BillingResultWrapper expectedBillingResult = BillingResultWrapper( @@ -149,10 +149,11 @@ void main() { await connection.queryProductDetails(['invalid'].toSet()); expect(response.notFoundIDs, ['invalid']); expect(response.productDetails, isEmpty); - expect(response.error.source, IAPSource.GooglePlay); - expect(response.error.code, 'error_code'); - expect(response.error.message, 'error_message'); - expect(response.error.details, {'info': 'error_info'}); + expect(response.error, isNotNull); + expect(response.error!.source, IAPSource.GooglePlay); + expect(response.error!.code, 'error_code'); + expect(response.error!.message, 'error_message'); + expect(response.error!.details, {'info': 'error_info'}); }); }); @@ -172,8 +173,10 @@ void main() { final QueryPurchaseDetailsResponse response = await connection.queryPastPurchases(); expect(response.pastPurchases, isEmpty); - expect(response.error.message, BillingResponse.developerError.toString()); - expect(response.error.source, IAPSource.GooglePlay); + expect(response.error, isNotNull); + expect( + response.error!.message, BillingResponse.developerError.toString()); + expect(response.error!.source, IAPSource.GooglePlay); }); test('returns SkuDetailsResponseWrapper', () async { @@ -221,9 +224,10 @@ void main() { final QueryPurchaseDetailsResponse response = await connection.queryPastPurchases(); expect(response.pastPurchases, isEmpty); - expect(response.error.code, 'error_code'); - expect(response.error.message, 'error_message'); - expect(response.error.details, {'info': 'error_info'}); + expect(response.error, isNotNull); + expect(response.error!.code, 'error_code'); + expect(response.error!.message, 'error_message'); + expect(response.error!.details, {'info': 'error_info'}); }); }); @@ -277,7 +281,7 @@ void main() { PurchaseDetails purchaseDetails; Stream purchaseStream = GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = purchaseStream.listen((_) { purchaseDetails = _.first; completer.complete(purchaseDetails); @@ -320,7 +324,7 @@ void main() { PurchaseDetails purchaseDetails; Stream purchaseStream = GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = purchaseStream.listen((_) { purchaseDetails = _.first; completer.complete(purchaseDetails); @@ -334,9 +338,9 @@ void main() { PurchaseDetails result = await completer.future; expect(result.error, isNotNull); - expect(result.error.source, IAPSource.GooglePlay); + expect(result.error!.source, IAPSource.GooglePlay); expect(result.status, PurchaseStatus.error); - expect(result.purchaseID, isNull); + expect(result.purchaseID, isEmpty); }); test('buy consumable with auto consume, serializes and deserializes data', @@ -392,7 +396,7 @@ void main() { PurchaseDetails purchaseDetails; Stream purchaseStream = GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = purchaseStream.listen((_) { purchaseDetails = _.first; completer.complete(purchaseDetails); @@ -407,7 +411,8 @@ void main() { // Verify that the result has succeeded PurchaseDetails result = await completer.future; expect(launchResult, isTrue); - expect(result.billingClientPurchase.purchaseToken, + expect(result.billingClientPurchase, isNotNull); + expect(result.billingClientPurchase!.purchaseToken, await consumeCompleter.future); expect(result.status, PurchaseStatus.purchased); expect(result.error, isNull); @@ -501,7 +506,7 @@ void main() { PurchaseDetails purchaseDetails; Stream purchaseStream = GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = purchaseStream.listen((_) { purchaseDetails = _.first; completer.complete(purchaseDetails); @@ -515,11 +520,12 @@ void main() { // Verify that the result has an error for the failed consumption PurchaseDetails result = await completer.future; - expect(result.billingClientPurchase.purchaseToken, + expect(result.billingClientPurchase, isNotNull); + expect(result.billingClientPurchase!.purchaseToken, await consumeCompleter.future); expect(result.status, PurchaseStatus.error); expect(result.error, isNotNull); - expect(result.error.code, kConsumptionFailedErrorCode); + expect(result.error!.code, kConsumptionFailedErrorCode); }); test( @@ -574,7 +580,7 @@ void main() { Stream purchaseStream = GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; + late StreamSubscription subscription; subscription = purchaseStream.listen((_) { consumeCompleter.complete(null); subscription.cancel(); @@ -629,10 +635,6 @@ void main() { await GooglePlayConnection.instance.completePurchase( purchaseDetails, developerPayload: 'dummy payload'); - print('pending ${billingResultWrapper.responseCode}'); - print('expectedBillingResult ${expectedBillingResult.responseCode}'); - print('pending ${billingResultWrapper.debugMessage}'); - print('expectedBillingResult ${expectedBillingResult.debugMessage}'); expect(billingResultWrapper, equals(expectedBillingResult)); completer.complete(billingResultWrapper); } diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart index 92ffbc5797e3..d41a1269d6c9 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -20,6 +20,10 @@ void main() { setUp(() {}); + tearDown(() { + fakeIOSPlatform.testReturnNull = false; + }); + group('sk_request_maker', () { test('get products method channel', () async { SkProductResponseWrapper productResponseWrapper = @@ -55,7 +59,7 @@ void main() { test('get products method channel should throw exception', () async { fakeIOSPlatform.getProductRequestFailTest = true; expect( - SKRequestMaker().startProductRequest(['xxx']), + SKRequestMaker().startProductRequest(['xxx']), throwsException, ); fakeIOSPlatform.getProductRequestFailTest = false; @@ -63,10 +67,11 @@ void main() { test('refreshed receipt', () async { int receiptCountBefore = fakeIOSPlatform.refreshReceipt; - await SKRequestMaker() - .startRefreshReceiptRequest(receiptProperties: {"isExpired": true}); + await SKRequestMaker().startRefreshReceiptRequest( + receiptProperties: {"isExpired": true}); expect(fakeIOSPlatform.refreshReceipt, receiptCountBefore + 1); - expect(fakeIOSPlatform.refreshReceiptParam, {"isExpired": true}); + expect(fakeIOSPlatform.refreshReceiptParam, + {"isExpired": true}); }); }); @@ -83,6 +88,12 @@ void main() { expect(await SKPaymentQueueWrapper.canMakePayments(), true); }); + test('canMakePayment returns false if method channel returns null', + () async { + fakeIOSPlatform.testReturnNull = true; + expect(await SKPaymentQueueWrapper.canMakePayments(), false); + }); + test('transactions should return a valid list of transactions', () async { expect(await SKPaymentQueueWrapper().transactions(), isNotEmpty); }); @@ -127,20 +138,20 @@ void main() { class FakeIOSPlatform { FakeIOSPlatform() { channel.setMockMethodCallHandler(onMethodCall); - getProductRequestFailTest = false; } // get product request - List startProductRequestParam; - bool getProductRequestFailTest; + List startProductRequestParam = []; + bool getProductRequestFailTest = false; + bool testReturnNull = false; // refresh receipt request int refreshReceipt = 0; - Map refreshReceiptParam; + late Map refreshReceiptParam; // payment queue List payments = []; List> transactionsFinished = []; - String applicationNameHasTransactionRestored; + String applicationNameHasTransactionRestored = ''; Future onMethodCall(MethodCall call) { switch (call.method) { @@ -157,18 +168,24 @@ class FakeIOSPlatform { buildProductResponseMap(dummyProductResponseWrapper)); case '-[InAppPurchasePlugin refreshReceipt:result:]': refreshReceipt++; - refreshReceiptParam = call.arguments; + refreshReceiptParam = + Map.castFrom(call.arguments); return Future.sync(() {}); // receipt manager case '-[InAppPurchasePlugin retrieveReceiptData:result:]': return Future.value('receipt data'); // payment queue case '-[SKPaymentQueue canMakePayments:]': + if (testReturnNull) { + return Future.value(null); + } return Future.value(true); case '-[SKPaymentQueue transactions]': - return Future>.value([buildTransactionMap(dummyTransaction)]); + return Future>.value( + [buildTransactionMap(dummyTransaction)]); case '-[InAppPurchasePlugin addPayment:result:]': - payments.add(SKPaymentWrapper.fromJson(call.arguments)); + payments.add(SKPaymentWrapper.fromJson( + Map.from(call.arguments))); return Future.sync(() {}); case '-[InAppPurchasePlugin finishTransaction:result:]': transactionsFinished.add(Map.from(call.arguments)); @@ -182,16 +199,18 @@ class FakeIOSPlatform { } class TestPaymentTransactionObserver extends SKTransactionObserverWrapper { - void updatedTransactions({List transactions}) {} + void updatedTransactions( + {required List transactions}) {} - void removedTransactions({List transactions}) {} + void removedTransactions( + {required List transactions}) {} - void restoreCompletedTransactionsFailed({SKError error}) {} + void restoreCompletedTransactionsFailed({required SKError error}) {} void paymentQueueRestoreCompletedTransactionsFinished() {} bool shouldAddStorePayment( - {SKPaymentWrapper payment, SKProductWrapper product}) { + {required SKPaymentWrapper payment, required SKProductWrapper product}) { return true; } } diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart index 2a9066f05c53..6e1f59bf377e 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart @@ -3,11 +3,11 @@ // found in the LICENSE file. import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; +import 'package:in_app_purchase/store_kit_wrappers.dart'; import 'package:test/test.dart'; import 'package:in_app_purchase/src/store_kit_wrappers/sk_product_wrapper.dart'; import 'package:in_app_purchase/src/in_app_purchase/in_app_purchase_connection.dart'; import 'package:in_app_purchase/src/in_app_purchase/product_details.dart'; -import 'package:in_app_purchase/store_kit_wrappers.dart'; import 'sk_test_stub_objects.dart'; void main() { @@ -17,17 +17,17 @@ void main() { () { final SKProductSubscriptionPeriodWrapper wrapper = SKProductSubscriptionPeriodWrapper.fromJson( - buildSubscriptionPeriodMap(dummySubscription)); + buildSubscriptionPeriodMap(dummySubscription)!); expect(wrapper, equals(dummySubscription)); }); test( - 'SKProductSubscriptionPeriodWrapper should have properties to be null if map is empty', + 'SKProductSubscriptionPeriodWrapper should have properties to be default values if map is empty', () { final SKProductSubscriptionPeriodWrapper wrapper = SKProductSubscriptionPeriodWrapper.fromJson({}); - expect(wrapper.numberOfUnits, null); - expect(wrapper.unit, null); + expect(wrapper.numberOfUnits, 0); + expect(wrapper.unit, SKSubscriptionPeriodUnit.day); }); test( @@ -39,15 +39,19 @@ void main() { }); test( - 'SKProductDiscountWrapper should have properties to be null if map is empty', + 'SKProductDiscountWrapper should have properties to be default if map is empty', () { final SKProductDiscountWrapper wrapper = SKProductDiscountWrapper.fromJson({}); - expect(wrapper.price, null); - expect(wrapper.priceLocale, null); - expect(wrapper.numberOfPeriods, null); - expect(wrapper.paymentMode, null); - expect(wrapper.subscriptionPeriod, null); + expect(wrapper.price, ''); + expect(wrapper.priceLocale, + SKPriceLocaleWrapper(currencyCode: '', currencySymbol: '')); + expect(wrapper.numberOfPeriods, 0); + expect(wrapper.paymentMode, SKProductDiscountPaymentMode.payAsYouGo); + expect( + wrapper.subscriptionPeriod, + SKProductSubscriptionPeriodWrapper( + numberOfUnits: 0, unit: SKSubscriptionPeriodUnit.day)); }); test('SKProductWrapper should have property values consistent with map', @@ -57,16 +61,18 @@ void main() { expect(wrapper, equals(dummyProductWrapper)); }); - test('SKProductWrapper should have properties to be null if map is empty', + test( + 'SKProductWrapper should have properties to be default if map is empty', () { final SKProductWrapper wrapper = SKProductWrapper.fromJson({}); - expect(wrapper.productIdentifier, null); - expect(wrapper.localizedTitle, null); - expect(wrapper.localizedDescription, null); - expect(wrapper.priceLocale, null); + expect(wrapper.productIdentifier, ''); + expect(wrapper.localizedTitle, ''); + expect(wrapper.localizedDescription, ''); + expect(wrapper.priceLocale, + SKPriceLocaleWrapper(currencyCode: '', currencySymbol: '')); expect(wrapper.subscriptionGroupIdentifier, null); - expect(wrapper.price, null); + expect(wrapper.price, ''); expect(wrapper.subscriptionPeriod, null); }); @@ -132,7 +138,8 @@ void main() { PurchaseDetails.fromSKTransaction(dummyTransaction, 'receipt data'); expect(dummyTransaction.transactionIdentifier, details.purchaseID); expect(dummyTransaction.payment.productIdentifier, details.productID); - expect((dummyTransaction.transactionTimeStamp * 1000).toInt().toString(), + expect(dummyTransaction.transactionTimeStamp, isNotNull); + expect((dummyTransaction.transactionTimeStamp! * 1000).toInt().toString(), details.transactionDate); expect(details.verificationData.localVerificationData, 'receipt data'); expect(details.verificationData.serverVerificationData, 'receipt data'); @@ -141,6 +148,29 @@ void main() { expect(details.billingClientPurchase, null); expect(details.pendingCompletePurchase, true); }); + + test('SKPaymentTransactionWrapper.toFinishMap set correct value', () { + final SKPaymentTransactionWrapper transactionWrapper = + SKPaymentTransactionWrapper( + payment: dummyPayment, + transactionState: SKPaymentTransactionStateWrapper.failed, + transactionIdentifier: 'abcd'); + final Map finishMap = transactionWrapper.toFinishMap(); + expect(finishMap['transactionIdentifier'], 'abcd'); + expect(finishMap['productIdentifier'], dummyPayment.productIdentifier); + }); + + test( + 'SKPaymentTransactionWrapper.toFinishMap should set transactionIdentifier to null when necessary', + () { + final SKPaymentTransactionWrapper transactionWrapper = + SKPaymentTransactionWrapper( + payment: dummyPayment, + transactionState: SKPaymentTransactionStateWrapper.failed); + final Map finishMap = transactionWrapper.toFinishMap(); + expect(finishMap['transactionIdentifier'], null); + }); + test('Should generate correct map of the payment object', () { Map map = dummyPayment.toMap(); expect(map['productIdentifier'], dummyPayment.productIdentifier); diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_test_stub_objects.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_test_stub_objects.dart index c976e80a90a5..f7d86f5cf59b 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_test_stub_objects.dart +++ b/packages/in_app_purchase/test/store_kit_wrappers/sk_test_stub_objects.dart @@ -74,8 +74,11 @@ Map buildLocaleMap(SKPriceLocaleWrapper local) { }; } -Map buildSubscriptionPeriodMap( - SKProductSubscriptionPeriodWrapper sub) { +Map? buildSubscriptionPeriodMap( + SKProductSubscriptionPeriodWrapper? sub) { + if (sub == null) { + return null; + } return { 'numberOfUnits': sub.numberOfUnits, 'unit': SKSubscriptionPeriodUnit.values.indexOf(sub.unit), @@ -104,7 +107,7 @@ Map buildProductMap(SKProductWrapper product) { 'price': product.price, 'subscriptionPeriod': buildSubscriptionPeriodMap(product.subscriptionPeriod), - 'introductoryPrice': buildDiscountMap(product.introductoryPrice), + 'introductoryPrice': buildDiscountMap(product.introductoryPrice!), }; } @@ -129,17 +132,16 @@ Map buildErrorMap(SKError error) { Map buildTransactionMap( SKPaymentTransactionWrapper transaction) { - if (transaction == null) { - return null; - } - Map map = { + Map map = { 'transactionState': SKPaymentTransactionStateWrapper.values .indexOf(SKPaymentTransactionStateWrapper.purchased), 'payment': transaction.payment.toMap(), - 'originalTransaction': buildTransactionMap(transaction.originalTransaction), + 'originalTransaction': transaction.originalTransaction == null + ? null + : buildTransactionMap(transaction.originalTransaction!), 'transactionTimeStamp': transaction.transactionTimeStamp, 'transactionIdentifier': transaction.transactionIdentifier, - 'error': buildErrorMap(transaction.error), + 'error': buildErrorMap(transaction.error!), }; return map; } diff --git a/packages/in_app_purchase/test/stub_in_app_purchase_platform.dart b/packages/in_app_purchase/test/stub_in_app_purchase_platform.dart index 312479573a68..431d8859d44d 100644 --- a/packages/in_app_purchase/test/stub_in_app_purchase_platform.dart +++ b/packages/in_app_purchase/test/stub_in_app_purchase_platform.dart @@ -9,19 +9,19 @@ typedef void AdditionalSteps(dynamic args); class StubInAppPurchasePlatform { Map _expectedCalls = {}; - Map _additionalSteps = {}; + Map _additionalSteps = {}; void addResponse( - {String name, + {required String name, dynamic value, - AdditionalSteps additionalStepBeforeReturn}) { + AdditionalSteps? additionalStepBeforeReturn}) { _additionalSteps[name] = additionalStepBeforeReturn; _expectedCalls[name] = value; } List _previousCalls = []; List get previousCalls => _previousCalls; - MethodCall previousCallMatching(String name) => _previousCalls - .firstWhere((MethodCall call) => call.method == name, orElse: () => null); + MethodCall previousCallMatching(String name) => + _previousCalls.firstWhere((MethodCall call) => call.method == name); int countPreviousCalls(String name) => _previousCalls.where((MethodCall call) => call.method == name).length; @@ -35,7 +35,7 @@ class StubInAppPurchasePlatform { _previousCalls.add(call); if (_expectedCalls.containsKey(call.method)) { if (_additionalSteps[call.method] != null) { - _additionalSteps[call.method](call.arguments); + _additionalSteps[call.method]!(call.arguments); } return Future.sync(() => _expectedCalls[call.method]); } else { diff --git a/packages/integration_test/CHANGELOG.md b/packages/integration_test/CHANGELOG.md index 534d7063db6c..71ab0e86266b 100644 --- a/packages/integration_test/CHANGELOG.md +++ b/packages/integration_test/CHANGELOG.md @@ -1,3 +1,27 @@ +## 1.0.2+2 + +* Fix tests from changes to `flutter test` machine output. + +## 1.0.2+1 + +* Update vm_service constraint + +## 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 be08a722bc75..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'; @@ -42,7 +44,7 @@ Future main() => integrationDriver(); You can also use different driver scripts to customize the behavior of the app under test. For example, `FlutterDriver` can also be parameterized with different [options](https://api.flutter.dev/flutter/flutter_driver/FlutterDriver/connect.html). -See the [extended driver](https://github.com/flutter/plugins/tree/master/packages/integration_test/example/test_driver/integration_test_extended_driver.dart) for an example. +See the [extended driver](https://github.com/flutter/plugins/blob/master/packages/integration_test/example/test_driver/extended_integration_test.dart) for an example. ### Package Structure diff --git a/packages/integration_test/android/gradle.properties b/packages/integration_test/android/gradle.properties index 2bd6f4fda009..8bd86f680510 100644 --- a/packages/integration_test/android/gradle.properties +++ b/packages/integration_test/android/gradle.properties @@ -1,2 +1 @@ org.gradle.jvmargs=-Xmx1536M - diff --git a/packages/integration_test/example/pubspec.yaml b/packages/integration_test/example/pubspec.yaml index 9384e9763935..5ad8be4a818e 100644 --- a/packages/integration_test/example/pubspec.yaml +++ b/packages/integration_test/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: 'none' environment: sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.6.7 <2.0.0" + flutter: ">=1.6.7" dependencies: flutter: @@ -18,6 +18,11 @@ dev_dependencies: flutter_driver: sdk: flutter integration_test: + # When depending on this package from a real application you should use: + # integration_test: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ integration_test_macos: path: ../integration_test_macos diff --git a/packages/integration_test/integration_test_macos/ios/integration_test_macos.podspec b/packages/integration_test/integration_test_macos/ios/integration_test_macos.podspec deleted file mode 100644 index 7294590a6479..000000000000 --- a/packages/integration_test/integration_test_macos/ios/integration_test_macos.podspec +++ /dev/null @@ -1,21 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'IntegrationTestMacOS' - s.version = '0.0.1' - s.summary = 'No-op implementation of the integration_test desktop plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of integration to avoid build issues on iOS. - See https://github.com/flutter/flutter/issues/39659 - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/integration_test/integration_test_macos' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end \ No newline at end of file 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..33c174a9724a 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+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: @@ -18,7 +18,7 @@ dependencies: # TODO(dnfield): This is a problem - flutter_driver and flutter_tools depend # on this packkage, and so does integration_test. When this gets rev'd in the # SDK, it has to be rev'd here too so integration tests in the SDK can use it. - vm_service: ">= 4.2.0 <6.0.0" + vm_service: ">= 4.2.0 <7.0.0" dev_dependencies: pedantic: ^1.8.0 diff --git a/packages/integration_test/test/binding_fail_test.dart b/packages/integration_test/test/binding_fail_test.dart index bb5961b18fc7..7ec176897c0c 100644 --- a/packages/integration_test/test/binding_fail_test.dart +++ b/packages/integration_test/test/binding_fail_test.dart @@ -61,13 +61,18 @@ Future> _runTest(String scriptPath) async { final String testResults = (await process.stdout .transform(utf8.decoder) .expand((String text) => text.split('\n')) - .map((String line) { + .map((String line) { try { return jsonDecode(line); } on FormatException { // Only interested in test events which are JSON. } }) + .expand>((dynamic json) { + return json is List + ? json.cast() + : >[json as Map]; + }) .where((dynamic testEvent) => testEvent != null && testEvent['type'] == 'print') .map((dynamic printEvent) => printEvent['message'] as String) diff --git a/packages/ios_platform_images/CHANGELOG.md b/packages/ios_platform_images/CHANGELOG.md index a83f22ec7571..bb87b7d6ff81 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.2.0 + +* Migrate to null safety. + +## 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/example/ios/Runner.xcodeproj/project.pbxproj b/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj index 03bbe666a0ed..ba0b25c0015b 100644 --- a/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj @@ -229,9 +229,12 @@ files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/ios_platform_images/ios_platform_images.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ios_platform_images.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -310,7 +313,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -387,7 +389,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -443,7 +444,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; diff --git a/packages/android_alarm_manager/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/ios_platform_images/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/ios_platform_images/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/ios_platform_images/example/lib/main.dart b/packages/ios_platform_images/example/lib/main.dart index 655380f8d125..394d983ab66c 100644 --- a/packages/ios_platform_images/example/lib/main.dart +++ b/packages/ios_platform_images/example/lib/main.dart @@ -14,8 +14,7 @@ class _MyAppState extends State { void initState() { super.initState(); - IosPlatformImages.resolveURL("textfile", null) - .then((value) => print(value)); + IosPlatformImages.resolveURL("textfile").then((value) => print(value)); } @override diff --git a/packages/ios_platform_images/example/pubspec.yaml b/packages/ios_platform_images/example/pubspec.yaml index fa0f9eb3aac6..552790ea74af 100644 --- a/packages/ios_platform_images/example/pubspec.yaml +++ b/packages/ios_platform_images/example/pubspec.yaml @@ -4,61 +4,25 @@ publish_to: 'none' homepage: https://github.com/flutter/plugins/tree/master/packages/ios_platform_images/ios_platform_images environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 + cupertino_icons: ^1.0.2 dev_dependencies: flutter_test: sdk: flutter ios_platform_images: + # When depending on this package from a real application you should use: + # ios_platform_images: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - pedantic: ^1.8.0 + pedantic: ^1.10.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, 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 - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/ios_platform_images/lib/ios_platform_images.dart b/packages/ios_platform_images/lib/ios_platform_images.dart index c7c12616ec36..d4599be94a64 100644 --- a/packages/ios_platform_images/lib/ios_platform_images.dart +++ b/packages/ios_platform_images/lib/ios_platform_images.dart @@ -1,3 +1,7 @@ +// Copyright 2020 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:typed_data'; import 'dart:ui' as ui; @@ -9,13 +13,11 @@ import 'package:flutter/foundation.dart' show SynchronousFuture, describeIdentity; class _FutureImageStreamCompleter extends ImageStreamCompleter { - final Future futureScale; - final InformationCollector informationCollector; - - _FutureImageStreamCompleter( - {Future codec, this.futureScale, this.informationCollector}) - : assert(codec != null), - assert(futureScale != null) { + _FutureImageStreamCompleter({ + required Future codec, + required this.futureScale, + this.informationCollector, + }) { codec.then(_onCodecReady, onError: (dynamic error, StackTrace stack) { reportError( context: ErrorDescription('resolving a single-frame image stream'), @@ -27,6 +29,9 @@ class _FutureImageStreamCompleter extends ImageStreamCompleter { }); } + final Future futureScale; + final InformationCollector? informationCollector; + Future _onCodecReady(ui.Codec codec) async { try { ui.FrameInfo nextFrame = await codec.getNextFrame(); @@ -50,9 +55,7 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { /// Constructor for FutureMemoryImage. [_futureBytes] is the bytes that will /// be loaded into an image and [_futureScale] is the scale that will be applied to /// that image to account for high-resolution images. - const _FutureMemoryImage(this._futureBytes, this._futureScale) - : assert(_futureBytes != null), - assert(_futureScale != null); + const _FutureMemoryImage(this._futureBytes, this._futureScale); final Future _futureBytes; final Future _futureScale; @@ -73,7 +76,9 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { } Future _loadAsync( - _FutureMemoryImage key, DecoderCallback decode) async { + _FutureMemoryImage key, + DecoderCallback decode, + ) async { assert(key == this); return _futureBytes.then((Uint8List bytes) { return decode(bytes); @@ -113,10 +118,19 @@ class IosPlatformImages { /// /// See [https://developer.apple.com/documentation/uikit/uiimage/1624146-imagenamed?language=objc] static ImageProvider load(String name) { - Future loadInfo = _channel.invokeMethod('loadImage', name); + Future loadInfo = _channel.invokeMapMethod('loadImage', name); Completer bytesCompleter = Completer(); Completer scaleCompleter = Completer(); loadInfo.then((map) { + if (map == null) { + scaleCompleter.completeError( + Exception("Image couldn't be found: $name"), + ); + bytesCompleter.completeError( + Exception("Image couldn't be found: $name"), + ); + return; + } scaleCompleter.complete(map["scale"]); bytesCompleter.complete(map["data"]); }); @@ -129,7 +143,7 @@ class IosPlatformImages { /// Returns null if the resource can't be found. /// /// See [https://developer.apple.com/documentation/foundation/nsbundle/1411540-urlforresource?language=objc] - static Future resolveURL(String name, [String ext]) { - return _channel.invokeMethod('resolveURL', [name, ext]); + static Future resolveURL(String name, {String? extension}) { + return _channel.invokeMethod('resolveURL', [name, extension]); } } diff --git a/packages/ios_platform_images/pubspec.yaml b/packages/ios_platform_images/pubspec.yaml index 482097a515b8..820542bcc362 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.2.0 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" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" dependencies: flutter: @@ -14,49 +14,10 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - # This section identifies this Flutter project as a plugin project. - # The androidPackage and pluginClass identifiers should not ordinarily - # be modified. They are used by the tooling to maintain consistency when - # adding or updating assets for this project. plugin: platforms: ios: pluginClass: IosPlatformImagesPlugin - - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # 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 - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/ios_platform_images/test/ios_platform_images_test.dart b/packages/ios_platform_images/test/ios_platform_images_test.dart index fd87180e9ac0..6ed7714e95c2 100644 --- a/packages/ios_platform_images/test/ios_platform_images_test.dart +++ b/packages/ios_platform_images/test/ios_platform_images_test.dart @@ -1,3 +1,7 @@ +// Copyright 2020 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'; import 'package:flutter_test/flutter_test.dart'; import 'package:ios_platform_images/ios_platform_images.dart'; diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md index 96388a76ab4f..b4d58395295f 100644 --- a/packages/local_auth/CHANGELOG.md +++ b/packages/local_auth/CHANGELOG.md @@ -1,3 +1,21 @@ +## 1.1.0 + +* Migrate to null safety. +* Allow pin, passcode, and pattern authentication with `authenticate` method. +* Fix incorrect error handling switch case fallthrough. +* Update README for Android Integration. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)). +* **Breaking change**. Parameter names refactored to use the generic `biometric` prefix in place of `fingerprint` in the `AndroidAuthMessages` class + * `fingerprintHint` is now `biometricHint` + * `fingerprintNotRecognized`is now `biometricNotRecognized` + * `fingerprintSuccess`is now `biometricSuccess` + * `fingerprintRequiredTitle` is now `biometricRequiredTitle` + +## 0.6.3+5 + +* Update Flutter SDK constraint. + ## 0.6.3+4 * Update Dart SDK constraint in example. diff --git a/packages/local_auth/README.md b/packages/local_auth/README.md index ca2aa49bed23..80820d759fec 100644 --- a/packages/local_auth/README.md +++ b/packages/local_auth/README.md @@ -23,8 +23,8 @@ bool canCheckBiometrics = Currently the following biometric types are implemented: -* BiometricType.face -* BiometricType.fingerprint +- BiometricType.face +- BiometricType.fingerprint To get a list of enrolled biometrics, call getAvailableBiometrics: @@ -44,10 +44,10 @@ if (Platform.isIOS) { We have default dialogs with an 'OK' button to show authentication error messages for the following 2 cases: -1. Passcode/PIN/Pattern Not Set. The user has not yet configured a passcode on - iOS or PIN/pattern on Android. -2. Touch ID/Fingerprint Not Enrolled. The user has not enrolled any - fingerprints on the device. +1. Passcode/PIN/Pattern Not Set. The user has not yet configured a passcode on + iOS or PIN/pattern on Android. +2. Touch ID/Fingerprint Not Enrolled. The user has not enrolled any + fingerprints on the device. Which means, if there's no fingerprint on the user's device, a dialog with instructions will pop up to let the user set up fingerprint. If the user clicks @@ -55,20 +55,33 @@ instructions will pop up to let the user set up fingerprint. If the user clicks Use the exported APIs to trigger local authentication with default dialogs: +The `authenticate()` method uses biometric authentication, but also allows +users to use pin, pattern, or passcode. + ```dart var localAuth = LocalAuthentication(); bool didAuthenticate = - await localAuth.authenticateWithBiometrics( + await localAuth.authenticate( localizedReason: 'Please authenticate to show account balance'); ``` +To authenticate using biometric authentication only, set `biometricOnly` to `true`. + +```dart +var localAuth = LocalAuthentication(); +bool didAuthenticate = + await localAuth.authenticate( + localizedReason: 'Please authenticate to show account balance', + biometricOnly: true); +``` + If you don't want to use the default dialogs, call this API with 'useErrorDialogs = false'. In this case, it will throw the error message back and you need to handle them in your dart code: ```dart bool didAuthenticate = - await localAuth.authenticateWithBiometrics( + await localAuth.authenticate( localizedReason: 'Please authenticate to show account balance', useErrorDialogs: false); ``` @@ -84,7 +97,7 @@ const iosStrings = const IOSAuthMessages( goToSettingsButton: 'settings', goToSettingsDescription: 'Please set up your Touch ID.', lockOut: 'Please reenable your Touch ID'); -await localAuth.authenticateWithBiometrics( +await localAuth.authenticate( localizedReason: 'Please authenticate to show account balance', useErrorDialogs: false, iOSAuthStrings: iosStrings); @@ -112,7 +125,7 @@ import 'package:flutter/services.dart'; import 'package:local_auth/error_codes.dart' as auth_error; try { - bool didAuthenticate = await local_auth.authenticateWithBiometrics( + bool didAuthenticate = await local_auth.authenticate( localizedReason: 'Please authenticate to show account balance'); } on PlatformException catch (e) { if (e.code == auth_error.notAvailable) { @@ -134,7 +147,6 @@ you need to also add: to your Info.plist file. Failure to do so results in a dialog that tells the user your app has not been updated to use TouchID. - ## Android Integration Note that local_auth plugin requires the use of a FragmentActivity as @@ -142,6 +154,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: @@ -155,7 +203,7 @@ Update your project's `AndroidManifest.xml` file to include the On Android, you can check only for existence of fingerprint hardware prior to API 29 (Android Q). Therefore, if you would like to support other biometrics types (such as face scanning) and you want to support SDKs lower than Q, -*do not* call `getAvailableBiometrics`. Simply call `authenticateWithBiometrics`. +_do not_ call `getAvailableBiometrics`. Simply call `authenticate` with `biometricOnly: true`. This will return an error if there was no hardware available. ## Sticky Auth @@ -170,6 +218,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/android/build.gradle b/packages/local_auth/android/build.gradle index 0fc603c36867..ae8e6f2828a2 100644 --- a/packages/local_auth/android/build.gradle +++ b/packages/local_auth/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:4.1.1' } } diff --git a/packages/local_auth/android/src/main/AndroidManifest.xml b/packages/local_auth/android/src/main/AndroidManifest.xml index b7da0caab6da..cb6cb985a986 100644 --- a/packages/local_auth/android/src/main/AndroidManifest.xml +++ b/packages/local_auth/android/src/main/AndroidManifest.xml @@ -1,3 +1,5 @@ + + diff --git a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java index e907cc43f2b4..3a7e2d76ca08 100644 --- a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java +++ b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java @@ -29,7 +29,7 @@ import java.util.concurrent.Executor; /** - * Authenticates the user with fingerprint and sends corresponding response back to Flutter. + * Authenticates the user with biometrics and sends corresponding response back to Flutter. * *

One instance per call is generated to ensure readable separation of executable paths across * method calls. @@ -37,10 +37,8 @@ @SuppressWarnings("deprecation") class AuthenticationHelper extends BiometricPrompt.AuthenticationCallback implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver { - /** The callback that handles the result of this authentication process. */ interface AuthCompletionHandler { - /** Called when authentication was successful. */ void onSuccess(); @@ -75,24 +73,32 @@ interface AuthCompletionHandler { Lifecycle lifecycle, FragmentActivity activity, MethodCall call, - AuthCompletionHandler completionHandler) { + AuthCompletionHandler completionHandler, + boolean allowCredentials) { this.lifecycle = lifecycle; this.activity = activity; this.completionHandler = completionHandler; this.call = call; this.isAuthSticky = call.argument("stickyAuth"); this.uiThreadExecutor = new UiThreadExecutor(); - this.promptInfo = + + BiometricPrompt.PromptInfo.Builder promptBuilder = new BiometricPrompt.PromptInfo.Builder() .setDescription((String) call.argument("localizedReason")) .setTitle((String) call.argument("signInTitle")) - .setSubtitle((String) call.argument("fingerprintHint")) - .setNegativeButtonText((String) call.argument("cancelButton")) + .setSubtitle((String) call.argument("biometricHint")) .setConfirmationRequired((Boolean) call.argument("sensitiveTransaction")) - .build(); + .setConfirmationRequired((Boolean) call.argument("sensitiveTransaction")); + + if (allowCredentials) { + promptBuilder.setDeviceCredentialAllowed(true); + } else { + promptBuilder.setNegativeButtonText((String) call.argument("cancelButton")); + } + this.promptInfo = promptBuilder.build(); } - /** Start the fingerprint listener. */ + /** Start the biometric listener. */ void authenticate() { if (lifecycle != null) { lifecycle.addObserver(this); @@ -103,7 +109,7 @@ void authenticate() { biometricPrompt.authenticate(promptInfo); } - /** Cancels the fingerprint authentication. */ + /** Cancels the biometric authentication. */ void stopAuthentication() { if (biometricPrompt != null) { biometricPrompt.cancelAuthentication(); @@ -111,7 +117,7 @@ void stopAuthentication() { } } - /** Stops the fingerprint listener. */ + /** Stops the biometric listener. */ private void stop() { if (lifecycle != null) { lifecycle.removeObserver(this); @@ -125,21 +131,28 @@ private void stop() { public void onAuthenticationError(int errorCode, CharSequence errString) { switch (errorCode) { case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL: - completionHandler.onError( - "PasscodeNotSet", - "Phone not secured by PIN, pattern or password, or SIM is currently locked."); + if (call.argument("useErrorDialogs")) { + showGoToSettingsDialog( + (String) call.argument("deviceCredentialsRequired"), + (String) call.argument("deviceCredentialsSetupDescription")); + return; + } + completionHandler.onError("NotAvailable", "Security credentials not available."); break; case BiometricPrompt.ERROR_NO_SPACE: case BiometricPrompt.ERROR_NO_BIOMETRICS: + if (promptInfo.isDeviceCredentialAllowed()) return; if (call.argument("useErrorDialogs")) { - showGoToSettingsDialog(); + showGoToSettingsDialog( + (String) call.argument("biometricRequired"), + (String) call.argument("goToSettingDescription")); return; } completionHandler.onError("NotEnrolled", "No Biometrics enrolled on this device."); break; case BiometricPrompt.ERROR_HW_UNAVAILABLE: case BiometricPrompt.ERROR_HW_NOT_PRESENT: - completionHandler.onError("NotAvailable", "Biometrics is not available on this device."); + completionHandler.onError("NotAvailable", "Security credentials not available."); break; case BiometricPrompt.ERROR_LOCKOUT: completionHandler.onError( @@ -176,7 +189,7 @@ public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult resul public void onAuthenticationFailed() {} /** - * If the activity is paused, we keep track because fingerprint dialog simply returns "User + * If the activity is paused, we keep track because biometric dialog simply returns "User * cancelled" when the activity is paused. */ @Override @@ -215,12 +228,12 @@ public void onResume(@NonNull LifecycleOwner owner) { // Suppress inflateParams lint because dialogs do not need to attach to a parent view. @SuppressLint("InflateParams") - private void showGoToSettingsDialog() { + private void showGoToSettingsDialog(String title, String descriptionText) { View view = LayoutInflater.from(activity).inflate(R.layout.go_to_setting, null, false); TextView message = (TextView) view.findViewById(R.id.fingerprint_required); TextView description = (TextView) view.findViewById(R.id.go_to_setting_description); - message.setText((String) call.argument("fingerprintRequired")); - description.setText((String) call.argument("goToSettingDescription")); + message.setText(title); + description.setText(descriptionText); Context context = new ContextThemeWrapper(activity, R.style.AlertDialogCustom); OnClickListener goToSettingHandler = new OnClickListener() { diff --git a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java index 0f93f40e947a..f4c6c168f54d 100644 --- a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java +++ b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java @@ -4,9 +4,18 @@ package io.flutter.plugins.localauth; +import static android.app.Activity.RESULT_OK; +import static android.content.Context.KEYGUARD_SERVICE; + import android.app.Activity; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; +import android.hardware.fingerprint.FingerprintManager; import android.os.Build; +import androidx.annotation.NonNull; +import androidx.biometric.BiometricManager; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.Lifecycle; import io.flutter.embedding.engine.plugins.FlutterPlugin; @@ -17,6 +26,8 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugin.common.PluginRegistry.Registrar; import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; @@ -29,14 +40,33 @@ @SuppressWarnings("deprecation") public class LocalAuthPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware { private static final String CHANNEL_NAME = "plugins.flutter.io/local_auth"; - + private static final int LOCK_REQUEST_CODE = 221; private Activity activity; private final AtomicBoolean authInProgress = new AtomicBoolean(false); - private AuthenticationHelper authenticationHelper; + private AuthenticationHelper authHelper; // These are null when not using v2 embedding. private MethodChannel channel; private Lifecycle lifecycle; + private BiometricManager biometricManager; + private FingerprintManager fingerprintManager; + private KeyguardManager keyguardManager; + private Result lockRequestResult; + private final PluginRegistry.ActivityResultListener resultListener = + new PluginRegistry.ActivityResultListener() { + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == LOCK_REQUEST_CODE) { + if (resultCode == RESULT_OK && lockRequestResult != null) { + authenticateSuccess(lockRequestResult); + } else { + authenticateFail(lockRequestResult); + } + lockRequestResult = null; + } + return false; + } + }; /** * Registers a plugin with the v1 embedding api {@code io.flutter.plugin.common}. @@ -49,13 +79,12 @@ public class LocalAuthPlugin implements MethodCallHandler, FlutterPlugin, Activi * io.flutter.plugin.common.BinaryMessenger}. */ @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME); - channel.setMethodCallHandler(new LocalAuthPlugin(registrar.activity())); - } - - private LocalAuthPlugin(Activity activity) { - this.activity = activity; + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + plugin.activity = registrar.activity(); + channel.setMethodCallHandler(plugin); + registrar.addActivityResultListener(plugin.resultListener); } /** @@ -66,118 +95,241 @@ private LocalAuthPlugin(Activity activity) { public LocalAuthPlugin() {} @Override - public void onMethodCall(MethodCall call, final Result result) { - if (call.method.equals("authenticateWithBiometrics")) { - if (authInProgress.get()) { - // Apps should not invoke another authentication request while one is in progress, - // so we classify this as an error condition. If we ever find a legitimate use case for - // this, we can try to cancel the ongoing auth and start a new one but for now, not worth - // the complexity. - result.error("auth_in_progress", "Authentication in progress", null); - return; - } + public void onMethodCall(MethodCall call, @NonNull final Result result) { + switch (call.method) { + case "authenticate": + authenticate(call, result); + break; + case "getAvailableBiometrics": + getAvailableBiometrics(result); + break; + case "isDeviceSupported": + isDeviceSupported(result); + break; + case "stopAuthentication": + stopAuthentication(result); + break; + default: + result.notImplemented(); + break; + } + } - if (activity == null || activity.isFinishing()) { - result.error("no_activity", "local_auth plugin requires a foreground activity", null); - return; - } + /* + * Starts authentication process + */ + private void authenticate(MethodCall call, final Result result) { + if (authInProgress.get()) { + result.error("auth_in_progress", "Authentication in progress", null); + return; + } - if (!(activity instanceof FragmentActivity)) { - result.error( - "no_fragment_activity", - "local_auth plugin requires activity to be a FragmentActivity.", - null); - return; - } - authInProgress.set(true); - authenticationHelper = - new AuthenticationHelper( - lifecycle, - (FragmentActivity) activity, - call, - new AuthCompletionHandler() { - @Override - public void onSuccess() { - if (authInProgress.compareAndSet(true, false)) { - result.success(true); - } - } - - @Override - public void onFailure() { - if (authInProgress.compareAndSet(true, false)) { - result.success(false); - } - } - - @Override - public void onError(String code, String error) { - if (authInProgress.compareAndSet(true, false)) { - result.error(code, error, null); - } - } - }); - authenticationHelper.authenticate(); - } else if (call.method.equals("getAvailableBiometrics")) { - try { - if (activity == null || activity.isFinishing()) { - result.error("no_activity", "local_auth plugin requires a foreground activity", null); - return; - } - ArrayList biometrics = new ArrayList(); - PackageManager packageManager = activity.getPackageManager(); - if (Build.VERSION.SDK_INT >= 23) { - if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { - biometrics.add("fingerprint"); + if (activity == null || activity.isFinishing()) { + result.error("no_activity", "local_auth plugin requires a foreground activity", null); + return; + } + + if (!(activity instanceof FragmentActivity)) { + result.error( + "no_fragment_activity", + "local_auth plugin requires activity to be a FragmentActivity.", + null); + return; + } + + if (!isDeviceSupported()) { + authInProgress.set(false); + result.error("NotAvailable", "Required security features not enabled", null); + return; + } + + authInProgress.set(true); + AuthCompletionHandler completionHandler = + new AuthCompletionHandler() { + @Override + public void onSuccess() { + authenticateSuccess(result); } - } - if (Build.VERSION.SDK_INT >= 29) { - if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) { - biometrics.add("face"); + + @Override + public void onFailure() { + authenticateFail(result); } - if (packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)) { - biometrics.add("iris"); + + @Override + public void onError(String code, String error) { + if (authInProgress.compareAndSet(true, false)) { + result.error(code, error, null); + } } + }; + + // if is biometricOnly try biometric prompt - might not work + boolean isBiometricOnly = call.argument("biometricOnly"); + if (isBiometricOnly) { + if (!canAuthenticateWithBiometrics()) { + if (!hasBiometricHardware()) { + completionHandler.onError("NoHardware", "No biometric hardware found"); } - result.success(biometrics); - } catch (Exception e) { - result.error("no_biometrics_available", e.getMessage(), null); + completionHandler.onError("NotEnrolled", "No biometrics enrolled on this device."); + return; } - } else if (call.method.equals(("stopAuthentication"))) { - stopAuthentication(result); - } else { - result.notImplemented(); + authHelper = + new AuthenticationHelper( + lifecycle, (FragmentActivity) activity, call, completionHandler, false); + authHelper.authenticate(); + return; + } + + // API 29 and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + authHelper = + new AuthenticationHelper( + lifecycle, (FragmentActivity) activity, call, completionHandler, true); + authHelper.authenticate(); + return; + } + + // API 23 - 28 with fingerprint + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && fingerprintManager != null) { + if (fingerprintManager.hasEnrolledFingerprints()) { + authHelper = + new AuthenticationHelper( + lifecycle, (FragmentActivity) activity, call, completionHandler, false); + authHelper.authenticate(); + return; + } + } + + // API 23 or higher with device credentials + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && keyguardManager != null + && keyguardManager.isDeviceSecure()) { + String title = call.argument("signInTitle"); + String reason = call.argument("localizedReason"); + Intent authIntent = keyguardManager.createConfirmDeviceCredentialIntent(title, reason); + + // save result for async response + lockRequestResult = result; + activity.startActivityForResult(authIntent, LOCK_REQUEST_CODE); + return; + } + + // Unable to authenticate + result.error("NotSupported", "This device does not support required security features", null); + } + + private void authenticateSuccess(Result result) { + if (authInProgress.compareAndSet(true, false)) { + result.success(true); + } + } + + private void authenticateFail(Result result) { + if (authInProgress.compareAndSet(true, false)) { + result.success(false); } } /* - Stops the authentication if in progress. - */ + * Stops the authentication if in progress. + */ private void stopAuthentication(Result result) { try { - if (authenticationHelper != null && authInProgress.get()) { - authenticationHelper.stopAuthentication(); - authenticationHelper = null; - result.success(true); - return; + if (authHelper != null && authInProgress.get()) { + authHelper.stopAuthentication(); + authHelper = null; } - result.success(false); + authInProgress.set(false); + result.success(true); } catch (Exception e) { result.success(false); } } + /* + * Returns biometric types available on device + */ + private void getAvailableBiometrics(final Result result) { + try { + if (activity == null || activity.isFinishing()) { + result.error("no_activity", "local_auth plugin requires a foreground activity", null); + return; + } + ArrayList biometrics = getAvailableBiometrics(); + result.success(biometrics); + } catch (Exception e) { + result.error("no_biometrics_available", e.getMessage(), null); + } + } + + private ArrayList getAvailableBiometrics() { + ArrayList biometrics = new ArrayList<>(); + if (activity == null || activity.isFinishing()) { + return biometrics; + } + PackageManager packageManager = activity.getPackageManager(); + if (Build.VERSION.SDK_INT >= 23) { + if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { + biometrics.add("fingerprint"); + } + } + if (Build.VERSION.SDK_INT >= 29) { + if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) { + biometrics.add("face"); + } + if (packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)) { + biometrics.add("iris"); + } + } + + return biometrics; + } + + private boolean isDeviceSupported() { + if (keyguardManager == null) return false; + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && keyguardManager.isDeviceSecure()); + } + + private boolean canAuthenticateWithBiometrics() { + if (biometricManager == null) return false; + return biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS; + } + + private boolean hasBiometricHardware() { + if (biometricManager == null) return false; + return biometricManager.canAuthenticate() != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE; + } + + private void isDeviceSupported(Result result) { + result.success(isDeviceSupported()); + } + @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME); + channel = new MethodChannel(binding.getFlutterEngine().getDartExecutor(), CHANNEL_NAME); + channel.setMethodCallHandler(this); } @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) {} + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {} + + private void setServicesFromActivity(Activity activity) { + if (activity == null) return; + this.activity = activity; + Context context = activity.getBaseContext(); + biometricManager = BiometricManager.from(activity); + keyguardManager = (KeyguardManager) context.getSystemService(KEYGUARD_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + fingerprintManager = + (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE); + } + } @Override public void onAttachedToActivity(ActivityPluginBinding binding) { - activity = binding.getActivity(); + binding.addActivityResultListener(resultListener); + setServicesFromActivity(binding.getActivity()); lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); channel.setMethodCallHandler(this); } @@ -185,18 +337,17 @@ public void onAttachedToActivity(ActivityPluginBinding binding) { @Override public void onDetachedFromActivityForConfigChanges() { lifecycle = null; - activity = null; } @Override public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { - activity = binding.getActivity(); + binding.addActivityResultListener(resultListener); + setServicesFromActivity(binding.getActivity()); lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); } @Override public void onDetachedFromActivity() { - activity = null; lifecycle = null; channel.setMethodCallHandler(null); } diff --git a/packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..186b71557c50 100644 --- a/packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists 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/android/build.gradle b/packages/local_auth/example/android/build.gradle index 541636cc492a..ea78cdf2c29c 100644 --- a/packages/local_auth/example/android/build.gradle +++ b/packages/local_auth/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:4.1.1' } } diff --git a/packages/local_auth/example/android/gradle.properties b/packages/local_auth/example/android/gradle.properties index a6738207fd15..7fe61a74cee0 100644 --- a/packages/local_auth/example/android/gradle.properties +++ b/packages/local_auth/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx1024m android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties index 562393332f6c..cd9fe1c68282 100644 --- a/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu May 30 07:21:52 NPT 2019 +#Sun Jan 03 14:07:08 CST 2021 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-6.5-all.zip diff --git a/packages/local_auth/example/android/settings_aar.gradle b/packages/local_auth/example/android/settings_aar.gradle new file mode 100644 index 000000000000..e7b4def49cb5 --- /dev/null +++ b/packages/local_auth/example/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/packages/local_auth/example/lib/main.dart b/packages/local_auth/example/lib/main.dart index 06e33b9853be..8cdad5b7eed3 100644 --- a/packages/local_auth/example/lib/main.dart +++ b/packages/local_auth/example/lib/main.dart @@ -21,16 +21,28 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { final LocalAuthentication auth = LocalAuthentication(); - bool _canCheckBiometrics; - List _availableBiometrics; + _SupportState _supportState = _SupportState.unknown; + bool? _canCheckBiometrics; + List? _availableBiometrics; String _authorized = 'Not Authorized'; bool _isAuthenticating = false; + @override + void initState() { + super.initState(); + auth.isDeviceSupported().then( + (isSupported) => setState(() => _supportState = isSupported + ? _SupportState.supported + : _SupportState.unsupported), + ); + } + 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 +53,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; @@ -61,8 +74,8 @@ class _MyAppState extends State { _isAuthenticating = true; _authorized = 'Authenticating'; }); - authenticated = await auth.authenticateWithBiometrics( - localizedReason: 'Scan your fingerprint to authenticate', + authenticated = await auth.authenticate( + localizedReason: 'Let OS determine authentication method', useErrorDialogs: true, stickyAuth: true); setState(() { @@ -71,6 +84,42 @@ class _MyAppState extends State { }); } on PlatformException catch (e) { print(e); + setState(() { + _isAuthenticating = false; + _authorized = "Error - ${e.message}"; + }); + return; + } + if (!mounted) return; + + setState( + () => _authorized = authenticated ? 'Authorized' : 'Not Authorized'); + } + + Future _authenticateWithBiometrics() async { + bool authenticated = false; + try { + setState(() { + _isAuthenticating = true; + _authorized = 'Authenticating'; + }); + authenticated = await auth.authenticate( + localizedReason: + 'Scan your fingerprint (or face or whatever) to authenticate', + useErrorDialogs: true, + stickyAuth: true, + biometricOnly: true); + setState(() { + _isAuthenticating = false; + _authorized = 'Authenticating'; + }); + } on PlatformException catch (e) { + print(e); + setState(() { + _isAuthenticating = false; + _authorized = "Error - ${e.message}"; + }); + return; } if (!mounted) return; @@ -80,39 +129,92 @@ class _MyAppState extends State { }); } - void _cancelAuthentication() { - auth.stopAuthentication(); + void _cancelAuthentication() async { + await auth.stopAuthentication(); + setState(() => _isAuthenticating = false); } @override Widget build(BuildContext context) { return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: ConstrainedBox( - constraints: const BoxConstraints.expand(), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: ListView( + padding: const EdgeInsets.only(top: 30), + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_supportState == _SupportState.unknown) + CircularProgressIndicator() + else if (_supportState == _SupportState.supported) + Text("This device is supported") + else + Text("This device is not supported"), + Divider(height: 100), Text('Can check biometrics: $_canCheckBiometrics\n'), - RaisedButton( + ElevatedButton( child: const Text('Check biometrics'), onPressed: _checkBiometrics, ), + Divider(height: 100), Text('Available biometrics: $_availableBiometrics\n'), - RaisedButton( + ElevatedButton( child: const Text('Get available biometrics'), onPressed: _getAvailableBiometrics, ), + Divider(height: 100), Text('Current State: $_authorized\n'), - RaisedButton( - child: Text(_isAuthenticating ? 'Cancel' : 'Authenticate'), - onPressed: - _isAuthenticating ? _cancelAuthentication : _authenticate, - ) - ])), - )); + (_isAuthenticating) + ? ElevatedButton( + onPressed: _cancelAuthentication, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Cancel Authentication"), + Icon(Icons.cancel), + ], + ), + ) + : Column( + children: [ + ElevatedButton( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Authenticate'), + Icon(Icons.perm_device_information), + ], + ), + onPressed: _authenticate, + ), + ElevatedButton( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_isAuthenticating + ? 'Cancel' + : 'Authenticate: biometrics only'), + Icon(Icons.fingerprint), + ], + ), + onPressed: _authenticateWithBiometrics, + ), + ], + ), + ], + ), + ], + ), + ), + ); } } + +enum _SupportState { + unknown, + supported, + unsupported, +} diff --git a/packages/local_auth/example/pubspec.yaml b/packages/local_auth/example/pubspec.yaml index 0861c51adbc6..d50940f6b63c 100644 --- a/packages/local_auth/example/pubspec.yaml +++ b/packages/local_auth/example/pubspec.yaml @@ -5,6 +5,11 @@ dependencies: flutter: sdk: flutter local_auth: + # When depending on this package from a real application you should use: + # local_auth: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -12,11 +17,11 @@ dev_dependencies: path: ../../integration_test flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/local_auth/integration_test/local_auth_test.dart b/packages/local_auth/integration_test/local_auth_test.dart index 47e5dfa67912..d6527c7601e4 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'; @@ -7,6 +13,9 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('canCheckBiometrics', (WidgetTester tester) async { - expect(LocalAuthentication().getAvailableBiometrics(), completion(isList)); + expect( + LocalAuthentication().getAvailableBiometrics(), + completion(isList), + ); }); } diff --git a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m index aa0c217ef543..cda49a7d68c3 100644 --- a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m @@ -22,10 +22,17 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"authenticateWithBiometrics" isEqualToString:call.method]) { - [self authenticateWithBiometrics:call.arguments withFlutterResult:result]; + if ([@"authenticate" isEqualToString:call.method]) { + bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue]; + if (isBiometricOnly) { + [self authenticateWithBiometrics:call.arguments withFlutterResult:result]; + } else { + [self authenticate:call.arguments withFlutterResult:result]; + } } else if ([@"getAvailableBiometrics" isEqualToString:call.method]) { [self getAvailableBiometrics:result]; + } else if ([@"isDeviceSupported" isEqualToString:call.method]) { + result(@YES); } else { result(FlutterMethodNotImplemented); } @@ -89,7 +96,6 @@ - (void)getAvailableBiometrics:(FlutterResult)result { } result(biometrics); } - - (void)authenticateWithBiometrics:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { LAContext *context = [[LAContext alloc] init]; @@ -130,6 +136,48 @@ - (void)authenticateWithBiometrics:(NSDictionary *)arguments } } +- (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { + LAContext *context = [[LAContext alloc] init]; + NSError *authError = nil; + _lastCallArgs = nil; + _lastResult = nil; + context.localizedFallbackTitle = @""; + + if (@available(iOS 9.0, *)) { + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { + [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication + localizedReason:arguments[@"localizedReason"] + reply:^(BOOL success, NSError *error) { + if (success) { + result(@YES); + } else { + switch (error.code) { + case LAErrorPasscodeNotSet: + case LAErrorTouchIDNotAvailable: + case LAErrorTouchIDNotEnrolled: + case LAErrorTouchIDLockout: + [self handleErrors:error + flutterArguments:arguments + withFlutterResult:result]; + return; + case LAErrorSystemCancel: + if ([arguments[@"stickyAuth"] boolValue]) { + self->_lastCallArgs = arguments; + self->_lastResult = result; + return; + } + } + result(@NO); + } + }]; + } else { + [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; + } + } else { + // Fallback on earlier versions + } +} + - (void)handleErrors:(NSError *)authError flutterArguments:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { diff --git a/packages/local_auth/lib/auth_strings.dart b/packages/local_auth/lib/auth_strings.dart index a8f34f88723c..855098b6aba4 100644 --- a/packages/local_auth/lib/auth_strings.dart +++ b/packages/local_auth/lib/auth_strings.dart @@ -15,38 +15,46 @@ import 'package:intl/intl.dart'; /// Provides default values for all messages. class AndroidAuthMessages { const AndroidAuthMessages({ - this.fingerprintHint, - this.fingerprintNotRecognized, - this.fingerprintSuccess, + this.biometricHint, + this.biometricNotRecognized, + this.biometricRequiredTitle, + this.biometricSuccess, this.cancelButton, - this.signInTitle, - this.fingerprintRequiredTitle, + this.deviceCredentialsRequiredTitle, + this.deviceCredentialsSetupDescription, this.goToSettingsButton, this.goToSettingsDescription, + this.signInTitle, }); - 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? biometricHint; + final String? biometricNotRecognized; + final String? biometricRequiredTitle; + final String? biometricSuccess; + final String? cancelButton; + final String? deviceCredentialsRequiredTitle; + final String? deviceCredentialsSetupDescription; + final String? goToSettingsButton; + final String? goToSettingsDescription; + final String? signInTitle; Map get args { return { - 'fingerprintHint': fingerprintHint ?? androidFingerprintHint, - 'fingerprintNotRecognized': - fingerprintNotRecognized ?? androidFingerprintNotRecognized, - 'fingerprintSuccess': fingerprintSuccess ?? androidFingerprintSuccess, + 'biometricHint': biometricHint ?? androidBiometricHint, + 'biometricNotRecognized': + biometricNotRecognized ?? androidBiometricNotRecognized, + 'biometricSuccess': biometricSuccess ?? androidBiometricSuccess, + 'biometricRequired': + biometricRequiredTitle ?? androidBiometricRequiredTitle, 'cancelButton': cancelButton ?? androidCancelButton, - 'signInTitle': signInTitle ?? androidSignInTitle, - 'fingerprintRequired': - fingerprintRequiredTitle ?? androidFingerprintRequiredTitle, + 'deviceCredentialsRequired': deviceCredentialsRequiredTitle ?? + androidDeviceCredentialsRequiredTitle, + 'deviceCredentialsSetupDescription': deviceCredentialsSetupDescription ?? + androidDeviceCredentialsSetupDescription, 'goToSetting': goToSettingsButton ?? goToSettings, 'goToSettingDescription': goToSettingsDescription ?? androidGoToSettingsDescription, + 'signInTitle': signInTitle ?? androidSignInTitle, }; } } @@ -62,10 +70,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 { @@ -80,16 +88,17 @@ class IOSAuthMessages { // Strings for local_authentication plugin. Currently supports English. // Intl.message must be string literals. -String get androidFingerprintHint => Intl.message('Touch sensor', - desc: 'Hint message advising the user how to scan their fingerprint. It is ' +String get androidBiometricHint => Intl.message('Verify identity', + desc: + 'Hint message advising the user how to authenticate with biometrics. It is ' 'used on Android side. Maximum 60 characters.'); -String get androidFingerprintNotRecognized => - Intl.message('Fingerprint not recognized. Try again.', +String get androidBiometricNotRecognized => + Intl.message('Not recognized. Try again.', desc: 'Message to let the user know that authentication was failed. It ' 'is used on Android side. Maximum 60 characters.'); -String get androidFingerprintSuccess => Intl.message('Fingerprint recognized.', +String get androidBiometricSuccess => Intl.message('Success', desc: 'Message to let the user know that authentication was successful. It ' 'is used on Android side. Maximum 60 characters.'); @@ -97,17 +106,26 @@ String get androidCancelButton => Intl.message('Cancel', desc: 'Message showed on a button that the user can click to leave the ' 'current dialog. It is used on Android side. Maximum 30 characters.'); -String get androidSignInTitle => Intl.message('Fingerprint Authentication', +String get androidSignInTitle => Intl.message('Authentication required', desc: 'Message showed as a title in a dialog which indicates the user ' - 'that they need to scan fingerprint to continue. It is used on ' + 'that they need to scan biometric to continue. It is used on ' 'Android side. Maximum 60 characters.'); -String get androidFingerprintRequiredTitle { - return Intl.message('Fingerprint required', - desc: 'Message showed as a title in a dialog which indicates the user ' - 'fingerprint is not set up yet on their device. It is used on Android' - ' side. Maximum 60 characters.'); -} +String get androidBiometricRequiredTitle => Intl.message('Biometric required', + desc: 'Message showed as a title in a dialog which indicates the user ' + 'has not set up biometric authentication on their device. It is used on Android' + ' side. Maximum 60 characters.'); + +String get androidDeviceCredentialsRequiredTitle => Intl.message( + 'Device credentials required', + desc: 'Message showed as a title in a dialog which indicates the user ' + 'has not set up credentials authentication on their device. It is used on Android' + ' side. Maximum 60 characters.'); + +String get androidDeviceCredentialsSetupDescription => Intl.message( + 'Device credentials required', + desc: 'Message advising the user to go to the settings and configure ' + 'device credentials on their device. It shows in a dialog on Android side.'); String get goToSettings => Intl.message('Go to settings', desc: 'Message showed on a button that the user can click to go to ' @@ -115,10 +133,10 @@ String get goToSettings => Intl.message('Go to settings', 'and iOS side. Maximum 30 characters.'); String get androidGoToSettingsDescription => Intl.message( - 'Fingerprint is not set up on your device. Go to ' - '\'Settings > Security\' to add your fingerprint.', + 'Biometric authentication is not set up on your device. Go to ' + '\'Settings > Security\' to add biometric authentication.', desc: 'Message advising the user to go to the settings and configure ' - 'fingerprint on their device. It shows in a dialog on Android side.'); + 'biometric on their device. It shows in a dialog on Android side.'); String get iOSLockOut => Intl.message( 'Biometric authentication is disabled. Please lock and unlock your screen to ' diff --git a/packages/local_auth/lib/error_codes.dart b/packages/local_auth/lib/error_codes.dart index 3f6f298ba4f3..3b080c13baf7 100644 --- a/packages/local_auth/lib/error_codes.dart +++ b/packages/local_auth/lib/error_codes.dart @@ -3,7 +3,7 @@ // found in the LICENSE file. // Exception codes for `PlatformException` returned by -// `authenticateWithBiometrics`. +// `authenticate`. /// Indicates that the user has not yet configured a passcode (iOS) or /// PIN/pattern/password (Android) on the device. diff --git a/packages/local_auth/lib/local_auth.dart b/packages/local_auth/lib/local_auth.dart index b2b03b920d64..f8a7228bcd8d 100644 --- a/packages/local_auth/lib/local_auth.dart +++ b/packages/local_auth/lib/local_auth.dart @@ -30,7 +30,29 @@ void setMockPathProviderPlatform(Platform platform) { /// A Flutter plugin for authenticating the user identity locally. class LocalAuthentication { - /// Authenticates the user with biometrics available on the device. + /// The `authenticateWithBiometrics` method has been deprecated. + /// Use `authenticate` with `biometricOnly: true` instead + @Deprecated("Use `authenticate` with `biometricOnly: true` instead") + Future authenticateWithBiometrics({ + required String localizedReason, + bool useErrorDialogs = true, + bool stickyAuth = false, + AndroidAuthMessages androidAuthStrings = const AndroidAuthMessages(), + IOSAuthMessages iOSAuthStrings = const IOSAuthMessages(), + bool sensitiveTransaction = true, + }) => + authenticate( + localizedReason: localizedReason, + useErrorDialogs: useErrorDialogs, + stickyAuth: stickyAuth, + androidAuthStrings: androidAuthStrings, + iOSAuthStrings: iOSAuthStrings, + sensitiveTransaction: sensitiveTransaction, + biometricOnly: true, + ); + + /// Authenticates the user with biometrics available on the device while also + /// allowing the user to use device authentication - pin, pattern, passcode. /// /// Returns a [Future] holding true, if the user successfully authenticated, /// false otherwise. @@ -62,17 +84,21 @@ class LocalAuthentication { /// dialog after the face is recognized to make sure the user meant to unlock /// their phone. /// + /// Setting [biometricOnly] to true prevents authenticates from using non-biometric + /// local authentication such as pin, passcode, and passcode. + /// /// Throws an [PlatformException] if there were technical problems with local /// authentication (e.g. lack of relevant hardware). This might throw /// [PlatformException] with error code [otherOperatingSystem] on the iOS /// simulator. - Future authenticateWithBiometrics({ - @required String localizedReason, + Future authenticate({ + required String localizedReason, bool useErrorDialogs = true, bool stickyAuth = false, AndroidAuthMessages androidAuthStrings = const AndroidAuthMessages(), IOSAuthMessages iOSAuthStrings = const IOSAuthMessages(), bool sensitiveTransaction = true, + bool biometricOnly = false, }) async { assert(localizedReason != null); final Map args = { @@ -80,6 +106,7 @@ class LocalAuthentication { 'useErrorDialogs': useErrorDialogs, 'stickyAuth': stickyAuth, 'sensitiveTransaction': sensitiveTransaction, + 'biometricOnly': biometricOnly, }; if (_platform.isIOS) { args.addAll(iOSAuthStrings.args); @@ -87,13 +114,13 @@ class LocalAuthentication { args.addAll(androidAuthStrings.args); } else { throw PlatformException( - code: otherOperatingSystem, - message: 'Local authentication does not support non-Android/iOS ' - 'operating systems.', - details: 'Your operating system is ${_platform.operatingSystem}'); + code: otherOperatingSystem, + message: 'Local authentication does not support non-Android/iOS ' + 'operating systems.', + details: 'Your operating system is ${_platform.operatingSystem}', + ); } - return await _channel.invokeMethod( - 'authenticateWithBiometrics', args); + return (await _channel.invokeMethod('authenticate', args)) ?? false; } /// Returns true if auth was cancelled successfully. @@ -101,20 +128,27 @@ 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'); + return await _channel.invokeMethod('stopAuthentication') ?? false; } - 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 true if device is capable of checking biometrics or is able to + /// fail over to device credentials. + /// + /// Returns a [Future] bool true or false: + Future isDeviceSupported() async => + (await _channel.invokeMethod('isDeviceSupported')) ?? false; + /// Returns a list of enrolled biometrics /// /// Returns a [Future] List with the following possibilities: @@ -122,8 +156,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) { switch (value) { diff --git a/packages/local_auth/pubspec.yaml b/packages/local_auth/pubspec.yaml index 6ba77aca6679..337006aa196f 100644 --- a/packages/local_auth/pubspec.yaml +++ b/packages/local_auth/pubspec.yaml @@ -1,8 +1,8 @@ name: local_auth -description: Flutter plugin for Android and iOS device authentication sensors - such as Fingerprint Reader and Touch ID. +description: Flutter plugin for Android and iOS devices to allow local + authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern. homepage: https://github.com/flutter/plugins/tree/master/packages/local_auth -version: 0.6.3+4 +version: 1.1.0 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 + intl: ^0.17.0 + platform: ^3.0.0 + flutter_plugin_android_lifecycle: ^2.0.0 dev_dependencies: integration_test: @@ -28,8 +28,8 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <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..f4bf9fe0314d 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) { @@ -30,61 +30,135 @@ void main() { log.clear(); }); - test('authenticate with no args on Android.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticateWithBiometrics( - localizedReason: 'Needs secure'); - expect( - log, - [ - isMethodCall('authenticateWithBiometrics', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - }..addAll(const AndroidAuthMessages().args)), - ], - ); - }); + group("With device auth fail over", () { + test('authenticate with no args on Android.', () async { + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); + await localAuthentication.authenticate( + localizedReason: 'Needs secure', + biometricOnly: true, + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': true, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); - test('authenticate with no args on iOS.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await localAuthentication.authenticateWithBiometrics( - localizedReason: 'Needs secure'); - expect( - log, - [ - isMethodCall('authenticateWithBiometrics', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - }..addAll(const IOSAuthMessages().args)), - ], - ); + test('authenticate with no args on iOS.', () async { + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); + await localAuthentication.authenticate( + localizedReason: 'Needs secure', + biometricOnly: true, + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': true, + }..addAll(const IOSAuthMessages().args)), + ], + ); + }); + + test('authenticate with no sensitive transaction.', () async { + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); + await localAuthentication.authenticate( + localizedReason: 'Insecure', + sensitiveTransaction: false, + useErrorDialogs: false, + biometricOnly: true, + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': true, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); }); - test('authenticate with no sensitive transaction.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticateWithBiometrics( - localizedReason: 'Insecure', - sensitiveTransaction: false, - useErrorDialogs: false, - ); - expect( - log, - [ - isMethodCall('authenticateWithBiometrics', - arguments: { - 'localizedReason': 'Insecure', - 'useErrorDialogs': false, - 'stickyAuth': false, - 'sensitiveTransaction': false, - }..addAll(const AndroidAuthMessages().args)), - ], - ); + group("With biometrics only", () { + test('authenticate with no args on Android.', () async { + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); + await localAuthentication.authenticate( + localizedReason: 'Needs secure', + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); + + test('authenticate with no args on iOS.', () async { + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); + await localAuthentication.authenticate( + localizedReason: 'Needs secure', + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + }..addAll(const IOSAuthMessages().args)), + ], + ); + }); + + test('authenticate with no sensitive transaction.', () async { + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); + await localAuthentication.authenticate( + localizedReason: 'Insecure', + sensitiveTransaction: false, + useErrorDialogs: false, + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': false, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); }); }); } diff --git a/packages/package_info/CHANGELOG.md b/packages/package_info/CHANGELOG.md index 21319fe4fbf1..14a9e26639d9 100644 --- a/packages/package_info/CHANGELOG.md +++ b/packages/package_info/CHANGELOG.md @@ -1,3 +1,15 @@ +## 2.0.0 + +* Migrate to null safety. + +## 0.4.3+4 + +* Ensure `IntegrationTestPlugin` is registered in `example` app, so Firebase Test Lab tests report test results correctly. [Issue](https://github.com/flutter/flutter/issues/74944). + +## 0.4.3+3 + +* Update Flutter SDK constraint. + ## 0.4.3+2 * Remove unused `test` dependency. diff --git a/packages/package_info/README.md b/packages/package_info/README.md index b5b2405a231a..a09db08dd484 100644 --- a/packages/package_info/README.md +++ b/packages/package_info/README.md @@ -3,13 +3,6 @@ This Flutter plugin provides an API for querying information about an application package. -**Please set your constraint to `package_info: '>=0.4.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The package_info plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.4.y+z`. -Please use `package_info: '>=0.4.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - # Usage You can use the PackageInfo to query information about the @@ -39,10 +32,10 @@ PackageInfo.fromPlatform().then((PackageInfo packageInfo) { ## Known Issue -As noted on [issue 20761](https://github.com/flutter/flutter/issues/20761#issuecomment-493434578), package_info on iOS -requires the Xcode build folder to be rebuilt after changes to the version string in `pubspec.yaml`. -Clean the Xcode build folder with: -`XCode Menu -> Product -> (Holding Option Key) Clean build folder`. +As noted on [issue 20761](https://github.com/flutter/flutter/issues/20761#issuecomment-493434578), package_info on iOS +requires the Xcode build folder to be rebuilt after changes to the version string in `pubspec.yaml`. +Clean the Xcode build folder with: +`XCode Menu -> Product -> (Holding Option Key) Clean build folder`. ## Issues and feedback diff --git a/packages/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.java b/packages/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.java index a32c50484838..eb669bf16109 100644 --- a/packages/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.java +++ b/packages/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.java @@ -5,6 +5,7 @@ package io.flutter.plugins.packageinfoexample; import android.os.Bundle; +import dev.flutter.plugins.integration_test.IntegrationTestPlugin; import io.flutter.app.FlutterActivity; import io.flutter.plugins.packageinfo.PackageInfoPlugin; @@ -14,5 +15,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); PackageInfoPlugin.registerWith( registrarFor("io.flutter.plugins.packageinfo.PackageInfoPlugin")); + IntegrationTestPlugin.registerWith( + registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); } } diff --git a/packages/package_info/example/integration_test/package_info_test.dart b/packages/package_info/example/integration_test/package_info_test.dart index 5038509ec84f..e70c8a5f0eca 100644 --- a/packages/package_info/example/integration_test/package_info_test.dart +++ b/packages/package_info/example/integration_test/package_info_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:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/package_info/example/lib/main.dart b/packages/package_info/example/lib/main.dart index 91ed910ef21d..18620617d558 100644 --- a/packages/package_info/example/lib/main.dart +++ b/packages/package_info/example/lib/main.dart @@ -25,7 +25,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; @@ -57,7 +57,7 @@ class _MyHomePageState extends State { Widget _infoTile(String title, String subtitle) { return ListTile( title: Text(title), - subtitle: Text(subtitle ?? 'Not set'), + subtitle: Text(subtitle.isNotEmpty ? subtitle : 'Not set'), ); } diff --git a/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj b/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj index 6e63b7eb69ae..3525d85d6678 100644 --- a/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj @@ -26,11 +26,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9A0CC0B8F23AFE5DF719BADB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED91D820ABAEDEBEFEBDBDA /* Pods_Runner.framework */; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -50,8 +46,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -72,7 +66,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; @@ -80,7 +73,6 @@ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; B3868D4F5169B9990BB5D1F5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; CED91D820ABAEDEBEFEBDBDA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,8 +80,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, 9A0CC0B8F23AFE5DF719BADB /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -145,8 +135,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -280,7 +268,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -329,10 +317,13 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/package_info/package_info.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/packages/package_info/example/pubspec.yaml b/packages/package_info/example/pubspec.yaml index 605c375e9a8e..a4691fcba518 100644 --- a/packages/package_info/example/pubspec.yaml +++ b/packages/package_info/example/pubspec.yaml @@ -5,6 +5,11 @@ dependencies: flutter: sdk: flutter package_info: + # When depending on this package from a real application you should use: + # package_info: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ integration_test: path: ../../integration_test @@ -12,11 +17,11 @@ dependencies: dev_dependencies: flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/package_info/example/test_driver/integration_test.dart b/packages/package_info/example/test_driver/integration_test.dart index f532c389a02b..437b3609d119 100644 --- a/packages/package_info/example/test_driver/integration_test.dart +++ b/packages/package_info/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:convert'; import 'dart:io'; diff --git a/packages/package_info/lib/package_info.dart b/packages/package_info/lib/package_info.dart index eaf28597e56c..5b7f4e573aa0 100644 --- a/packages/package_info/lib/package_info.dart +++ b/packages/package_info/lib/package_info.dart @@ -24,30 +24,31 @@ class PackageInfo { /// See [fromPlatform] for the right API to get a [PackageInfo] that's /// actually populated with real data. PackageInfo({ - this.appName, - this.packageName, - this.version, - this.buildNumber, + required this.appName, + required this.packageName, + required this.version, + required this.buildNumber, }); - static PackageInfo _fromPlatform; + static PackageInfo? _fromPlatform; /// Retrieves package information from the platform. /// The result is cached. static Future fromPlatform() async { - if (_fromPlatform != null) { - return _fromPlatform; - } + PackageInfo? packageInfo = _fromPlatform; + if (packageInfo != null) return packageInfo; final Map map = - await _kChannel.invokeMapMethod('getAll'); - _fromPlatform = PackageInfo( - appName: map["appName"], - packageName: map["packageName"], - version: map["version"], - buildNumber: map["buildNumber"], + (await _kChannel.invokeMapMethod('getAll'))!; + + packageInfo = PackageInfo( + appName: map["appName"] ?? '', + packageName: map["packageName"] ?? '', + version: map["version"] ?? '', + buildNumber: map["buildNumber"] ?? '', ); - return _fromPlatform; + _fromPlatform = packageInfo; + return packageInfo; } /// The app name. `CFBundleDisplayName` on iOS, `application/label` on Android. diff --git a/packages/package_info/pubspec.yaml b/packages/package_info/pubspec.yaml index cb46ca66b9a4..2769af1f83d5 100644 --- a/packages/package_info/pubspec.yaml +++ b/packages/package_info/pubspec.yaml @@ -2,10 +2,7 @@ name: package_info description: Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android. 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: 2.0.0 flutter: plugin: @@ -29,8 +26,8 @@ dev_dependencies: sdk: flutter integration_test: path: ../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/package_info/test/package_info_test.dart b/packages/package_info/test/package_info_test.dart index 47d48fde2d2d..cb6967155ca0 100644 --- a/packages/package_info/test/package_info_test.dart +++ b/packages/package_info/test/package_info_test.dart @@ -11,7 +11,7 @@ void main() { const MethodChannel channel = MethodChannel('plugins.flutter.io/package_info'); - List log; + late List log; channel.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index 646a2e36bc9e..c28c617bbea4 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,27 @@ +## 2.0.0 + +* Migrate to null safety. +* BREAKING CHANGE: Path accessors that return non-nullable results will throw + a `MissingPlatformDirectoryException` if the platform implementation is unable + to get the corresponding directory (except on platforms where the method is + explicitly unsupported, where they will continue to throw `UnsupportedError`). + +## 1.6.28 + +* Drop unused UUID dependency for tests. + +## 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/integration_test/path_provider_test.dart b/packages/path_provider/path_provider/example/integration_test/path_provider_test.dart index 8eb8520b5b4b..2b12c82f959b 100644 --- a/packages/path_provider/path_provider/example/integration_test/path_provider_test.dart +++ b/packages/path_provider/path_provider/example/integration_test/path_provider_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:io'; diff --git a/packages/path_provider/path_provider/example/lib/main.dart b/packages/path_provider/path_provider/example/lib/main.dart index ce496e9d4e63..ddc1f8a6e2d5 100644 --- a/packages/path_provider/path_provider/example/lib/main.dart +++ b/packages/path_provider/path_provider/example/lib/main.dart @@ -28,7 +28,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 @@ -36,13 +36,13 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - Future _tempDirectory; - Future _appSupportDirectory; - Future _appLibraryDirectory; - Future _appDocumentsDirectory; - Future _externalDocumentsDirectory; - Future> _externalStorageDirectories; - Future> _externalCacheDirectories; + Future? _tempDirectory; + Future? _appSupportDirectory; + Future? _appLibraryDirectory; + Future? _appDocumentsDirectory; + Future? _externalDocumentsDirectory; + Future?>? _externalStorageDirectories; + Future?>? _externalCacheDirectories; void _requestTempDirectory() { setState(() { @@ -51,13 +51,13 @@ class _MyHomePageState extends State { } Widget _buildDirectory( - BuildContext context, AsyncSnapshot snapshot) { + BuildContext context, AsyncSnapshot snapshot) { Text text = const Text(''); if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError) { text = Text('Error: ${snapshot.error}'); } else if (snapshot.hasData) { - text = Text('path: ${snapshot.data.path}'); + text = Text('path: ${snapshot.data!.path}'); } else { text = const Text('path unavailable'); } @@ -66,14 +66,14 @@ class _MyHomePageState extends State { } Widget _buildDirectories( - BuildContext context, AsyncSnapshot> snapshot) { + BuildContext context, AsyncSnapshot?> snapshot) { Text text = const Text(''); if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError) { text = Text('Error: ${snapshot.error}'); } else if (snapshot.hasData) { final String combined = - snapshot.data.map((Directory d) => d.path).join(', '); + snapshot.data!.map((Directory d) => d.path).join(', '); text = Text('paths: $combined'); } else { text = const Text('path unavailable'); @@ -129,55 +129,55 @@ class _MyHomePageState extends State { children: [ Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Temporary Directory'), onPressed: _requestTempDirectory, ), ), - FutureBuilder( + FutureBuilder( future: _tempDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Application Documents Directory'), onPressed: _requestAppDocumentsDirectory, ), ), - FutureBuilder( + FutureBuilder( future: _appDocumentsDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Application Support Directory'), onPressed: _requestAppSupportDirectory, ), ), - FutureBuilder( + FutureBuilder( future: _appSupportDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Application Library Directory'), onPressed: _requestAppLibraryDirectory, ), ), - FutureBuilder( + FutureBuilder( 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: Platform.isIOS ? null : _requestExternalStorageDirectory, ), ), - FutureBuilder( + FutureBuilder( future: _externalDocumentsDirectory, builder: _buildDirectory), 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 @@ -190,13 +190,13 @@ class _MyHomePageState extends State { ), ), ]), - FutureBuilder>( + FutureBuilder?>( future: _externalStorageDirectories, builder: _buildDirectories), 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: @@ -204,7 +204,7 @@ class _MyHomePageState extends State { ), ), ]), - FutureBuilder>( + FutureBuilder?>( future: _externalCacheDirectories, builder: _buildDirectories), ], ), diff --git a/packages/path_provider/path_provider/example/pubspec.yaml b/packages/path_provider/path_provider/example/pubspec.yaml index 8659da753e15..68c751a81843 100644 --- a/packages/path_provider/path_provider/example/pubspec.yaml +++ b/packages/path_provider/path_provider/example/pubspec.yaml @@ -5,6 +5,11 @@ dependencies: flutter: sdk: flutter path_provider: + # When depending on this package from a real application you should use: + # path_provider: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -12,11 +17,11 @@ dev_dependencies: path: ../../../integration_test flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/path_provider/path_provider/example/test_driver/integration_test.dart b/packages/path_provider/path_provider/example/test_driver/integration_test.dart index 7a2c21338786..ac106b63b339 100644 --- a/packages/path_provider/path_provider/example/test_driver/integration_test.dart +++ b/packages/path_provider/path_provider/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/path_provider/path_provider/integration_test/path_provider_test.dart b/packages/path_provider/path_provider/integration_test/path_provider_test.dart index 18570aeca57e..da368d52b832 100644 --- a/packages/path_provider/path_provider/integration_test/path_provider_test.dart +++ b/packages/path_provider/path_provider/integration_test/path_provider_test.dart @@ -1,3 +1,9 @@ +// Copyright 2019 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. + +// @dart=2.9 + import 'package:flutter_test/flutter_test.dart'; import 'package:path_provider/path_provider.dart'; import 'package:integration_test/integration_test.dart'; diff --git a/packages/path_provider/path_provider/lib/path_provider.dart b/packages/path_provider/path_provider/lib/path_provider.dart index 0fbab57700be..da9c0b3d48a3 100644 --- a/packages/path_provider/path_provider/lib/path_provider.dart +++ b/packages/path_provider/path_provider/lib/path_provider.dart @@ -20,6 +20,27 @@ set disablePathProviderPlatformOverride(bool override) {} bool _manualDartRegistrationNeeded = true; +/// An exception thrown when a directory that should always be available on +/// the current platform cannot be obtained. +class MissingPlatformDirectoryException implements Exception { + /// Creates a new exception + MissingPlatformDirectoryException(this.message, {this.details}); + + /// The explanation of the exception. + final String message; + + /// Added details, if any. + /// + /// E.g., an error object from the platform implementation. + final Object? details; + + @override + String toString() { + String detailsAddition = details == null ? '' : ': $details'; + return 'MissingPlatformDirectoryException($message)$detailsAddition'; + } +} + PathProviderPlatform get _platform { // This is to manually endorse Dart implementations until automatic // registration of Dart plugins is implemented. For details see @@ -51,10 +72,14 @@ PathProviderPlatform get _platform { /// On iOS, this uses the `NSCachesDirectory` API. /// /// On Android, this uses the `getCacheDir` API on the context. +/// +/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// provide the directory. Future getTemporaryDirectory() async { - final String path = await _platform.getTemporaryPath(); + final String? path = await _platform.getTemporaryPath(); if (path == null) { - return null; + throw MissingPlatformDirectoryException( + 'Unable to get temporary directory'); } return Directory(path); } @@ -69,10 +94,14 @@ Future getTemporaryDirectory() async { /// If this directory does not exist, it is created automatically. /// /// On Android, this function uses the `getFilesDir` API on the context. +/// +/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// provide the directory. Future getApplicationSupportDirectory() async { - final String path = await _platform.getApplicationSupportPath(); + final String? path = await _platform.getApplicationSupportPath(); if (path == null) { - return null; + throw MissingPlatformDirectoryException( + 'Unable to get application support directory'); } return Directory(path); @@ -83,10 +112,13 @@ Future getApplicationSupportDirectory() async { /// /// On Android, this function throws an [UnsupportedError] as no equivalent /// path exists. +/// +/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// provide the directory on a supported platform. Future getLibraryDirectory() async { - final String path = await _platform.getLibraryPath(); + final String? path = await _platform.getLibraryPath(); if (path == null) { - return null; + throw MissingPlatformDirectoryException('Unable to get library directory'); } return Directory(path); } @@ -100,10 +132,14 @@ Future getLibraryDirectory() async { /// On Android, this uses the `getDataDirectory` API on the context. Consider /// using [getExternalStorageDirectory] instead if data is intended to be visible /// to the user. +/// +/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// provide the directory. Future getApplicationDocumentsDirectory() async { - final String path = await _platform.getApplicationDocumentsPath(); + final String? path = await _platform.getApplicationDocumentsPath(); if (path == null) { - return null; + throw MissingPlatformDirectoryException( + 'Unable to get application documents directory'); } return Directory(path); } @@ -116,8 +152,8 @@ Future getApplicationDocumentsDirectory() async { /// to access outside the app's sandbox. /// /// On Android this uses the `getExternalFilesDir(null)`. -Future getExternalStorageDirectory() async { - final String path = await _platform.getExternalStoragePath(); +Future getExternalStorageDirectory() async { + final String? path = await _platform.getExternalStoragePath(); if (path == null) { return null; } @@ -137,8 +173,11 @@ Future getExternalStorageDirectory() async { /// /// On Android this returns Context.getExternalCacheDirs() or /// Context.getExternalCacheDir() on API levels below 19. -Future> getExternalCacheDirectories() async { - final List paths = await _platform.getExternalCachePaths(); +Future?> getExternalCacheDirectories() async { + final List? paths = await _platform.getExternalCachePaths(); + if (paths == null) { + return null; + } return paths.map((String path) => Directory(path)).toList(); } @@ -155,13 +194,16 @@ Future> getExternalCacheDirectories() async { /// /// On Android this returns Context.getExternalFilesDirs(String type) or /// Context.getExternalFilesDir(String type) on API levels below 19. -Future> getExternalStorageDirectories({ +Future?> getExternalStorageDirectories({ /// Optional parameter. See [StorageDirectory] for more informations on /// how this type translates to Android storage directories. - StorageDirectory type, + StorageDirectory? type, }) async { - final List paths = + final List? paths = await _platform.getExternalStoragePaths(type: type); + if (paths == null) { + return null; + } return paths.map((String path) => Directory(path)).toList(); } @@ -171,8 +213,8 @@ Future> getExternalStorageDirectories({ /// /// On Android and on iOS, this function throws an [UnsupportedError] as no equivalent /// path exists. -Future getDownloadsDirectory() async { - final String path = await _platform.getDownloadsPath(); +Future getDownloadsDirectory() async { + final String? path = await _platform.getDownloadsPath(); if (path == null) { return null; } diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index b3960941acff..81941dac67b1 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: 2.0.0 flutter: plugin: @@ -21,10 +21,10 @@ flutter: dependencies: flutter: sdk: flutter - path_provider_platform_interface: ^1.0.1 - path_provider_macos: ^0.0.4 - path_provider_linux: ^0.0.1 - path_provider_windows: ^0.0.4 + path_provider_platform_interface: ^2.0.0 + path_provider_macos: ^2.0.0 + path_provider_linux: ^2.0.0 + path_provider_windows: ^2.0.0 dev_dependencies: integration_test: @@ -33,11 +33,10 @@ dev_dependencies: sdk: flutter flutter_driver: sdk: flutter - uuid: "^1.0.0" - pedantic: ^1.8.0 - mockito: ^4.1.1 - plugin_platform_interface: ^1.0.0 + pedantic: ^1.10.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" + test: ^1.16.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/path_provider/path_provider/test/path_provider_test.dart b/packages/path_provider/path_provider/test/path_provider_test.dart index eb17178b9975..759e9c09ffc2 100644 --- a/packages/path_provider/path_provider/test/path_provider_test.dart +++ b/packages/path_provider/path_provider/test/path_provider_test.dart @@ -6,10 +6,10 @@ import 'dart:io' show Directory; import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:test/fake.dart'; const String kTemporaryPath = 'temporaryPath'; const String kApplicationSupportPath = 'applicationSupportPath'; @@ -20,11 +20,10 @@ const String kExternalCachePath = 'externalCachePath'; const String kExternalStoragePath = 'externalStoragePath'; void main() { - group('PathProvider', () { - TestWidgetsFlutterBinding.ensureInitialized(); - + TestWidgetsFlutterBinding.ensureInitialized(); + group('PathProvider full implementation', () { setUp(() async { - PathProviderPlatform.instance = MockPathProviderPlatform(); + PathProviderPlatform.instance = FakePathProviderPlatform(); }); test('getTemporaryDirectory', () async { @@ -48,63 +47,147 @@ void main() { }); test('getExternalStorageDirectory', () async { - Directory result = await getExternalStorageDirectory(); - expect(result.path, kExternalStoragePath); + Directory? result = await getExternalStorageDirectory(); + expect(result?.path, kExternalStoragePath); }); test('getExternalCacheDirectories', () async { - List result = await getExternalCacheDirectories(); - expect(result.length, 1); - expect(result.first.path, kExternalCachePath); + List? result = await getExternalCacheDirectories(); + expect(result?.length, 1); + expect(result?.first.path, kExternalCachePath); }); test('getExternalStorageDirectories', () async { - List result = await getExternalStorageDirectories(); - expect(result.length, 1); - expect(result.first.path, kExternalStoragePath); + List? result = await getExternalStorageDirectories(); + expect(result?.length, 1); + expect(result?.first.path, kExternalStoragePath); }); test('getDownloadsDirectory', () async { - Directory result = await getDownloadsDirectory(); - expect(result.path, kDownloadsPath); + Directory? result = await getDownloadsDirectory(); + expect(result?.path, kDownloadsPath); + }); + }); + + group('PathProvider null implementation', () { + setUp(() async { + PathProviderPlatform.instance = AllNullFakePathProviderPlatform(); + }); + + test('getTemporaryDirectory throws on null', () async { + expect(getTemporaryDirectory(), + throwsA(isA())); + }); + + test('getApplicationSupportDirectory throws on null', () async { + expect(getApplicationSupportDirectory(), + throwsA(isA())); + }); + + test('getLibraryDirectory throws on null', () async { + expect(getLibraryDirectory(), + throwsA(isA())); + }); + + test('getApplicationDocumentsDirectory throws on null', () async { + expect(getApplicationDocumentsDirectory(), + throwsA(isA())); + }); + + test('getExternalStorageDirectory passes null through', () async { + Directory? result = await getExternalStorageDirectory(); + expect(result, isNull); + }); + + test('getExternalCacheDirectories passes null through', () async { + List? result = await getExternalCacheDirectories(); + expect(result, isNull); + }); + + test('getExternalStorageDirectories passes null through', () async { + List? result = await getExternalStorageDirectories(); + expect(result, isNull); + }); + + test('getDownloadsDirectory passses null through', () async { + Directory? result = await getDownloadsDirectory(); + expect(result, isNull); }); }); } -class MockPathProviderPlatform extends Mock +class FakePathProviderPlatform extends Fake with MockPlatformInterfaceMixin implements PathProviderPlatform { - Future getTemporaryPath() async { + Future getTemporaryPath() async { return kTemporaryPath; } - Future getApplicationSupportPath() async { + Future getApplicationSupportPath() async { return kApplicationSupportPath; } - Future getLibraryPath() async { + Future getLibraryPath() async { return kLibraryPath; } - Future getApplicationDocumentsPath() async { + Future getApplicationDocumentsPath() async { return kApplicationDocumentsPath; } - Future getExternalStoragePath() async { + Future getExternalStoragePath() async { return kExternalStoragePath; } - Future> getExternalCachePaths() async { + Future?> getExternalCachePaths() async { return [kExternalCachePath]; } - Future> getExternalStoragePaths({ - StorageDirectory type, + Future?> getExternalStoragePaths({ + StorageDirectory? type, }) async { return [kExternalStoragePath]; } - Future getDownloadsPath() async { + Future getDownloadsPath() async { return kDownloadsPath; } } + +class AllNullFakePathProviderPlatform extends Fake + with MockPlatformInterfaceMixin + implements PathProviderPlatform { + Future getTemporaryPath() async { + return null; + } + + Future getApplicationSupportPath() async { + return null; + } + + Future getLibraryPath() async { + return null; + } + + Future getApplicationDocumentsPath() async { + return null; + } + + Future getExternalStoragePath() async { + return null; + } + + Future?> getExternalCachePaths() async { + return null; + } + + Future?> getExternalStoragePaths({ + StorageDirectory? type, + }) async { + return null; + } + + Future getDownloadsPath() async { + return null; + } +} diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index 21f336c5f0f2..a85c6bbb4ef3 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,3 +1,15 @@ +## 2.0.0 + +* Migrate to null safety. + +## 0.1.1+3 + +* Update Flutter SDK constraint. + +## 0.1.1+2 + +* Log errors in the example when calls to the `path_provider` fail. + ## 0.1.1+1 * Check in linux/ directory for example/ @@ -19,4 +31,3 @@ ## 0.0.1 * The initial implementation of path_provider for Linux * Implements getApplicationSupportPath, getApplicationDocumentsPath, getDownloadsPath, and getTemporaryPath - diff --git a/packages/path_provider/path_provider_linux/README.md b/packages/path_provider/path_provider_linux/README.md index 373925d2d96d..ef9e0e855c86 100644 --- a/packages/path_provider/path_provider_linux/README.md +++ b/packages/path_provider/path_provider_linux/README.md @@ -2,13 +2,6 @@ The linux implementation of [`path_provider`]. -**Please set your constraint to `path_provider: '>=0.0.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The `path_provider` plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. -Please use `path_provider: '>=0.0.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage This package is already included as part of the `path_provider` package dependency, and will diff --git a/packages/path_provider/path_provider_linux/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_linux/example/integration_test/path_provider_test.dart index 18ac49debbd4..d08b3878a4d5 100644 --- a/packages/path_provider/path_provider_linux/example/integration_test/path_provider_test.dart +++ b/packages/path_provider/path_provider_linux/example/integration_test/path_provider_test.dart @@ -2,16 +2,19 @@ // 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:io'; import 'package:flutter_test/flutter_test.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:path_provider_linux/path_provider_linux.dart'; import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('getTemporaryDirectory', (WidgetTester tester) async { - final Directory result = await getTemporaryDirectory(); + final PathProviderLinux provider = PathProviderLinux(); + final String result = await provider.getTemporaryPath(); _verifySampleFile(result, 'temporaryDirectory'); }); @@ -19,25 +22,29 @@ void main() { if (!Platform.isLinux) { return; } - final Directory result = await getDownloadsDirectory(); + final PathProviderLinux provider = PathProviderLinux(); + final String result = await provider.getDownloadsPath(); _verifySampleFile(result, 'downloadDirectory'); }); testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { - final Directory result = await getApplicationDocumentsDirectory(); + final PathProviderLinux provider = PathProviderLinux(); + final String result = await provider.getApplicationDocumentsPath(); _verifySampleFile(result, 'applicationDocuments'); }); testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { - final Directory result = await getApplicationSupportDirectory(); + final PathProviderLinux provider = PathProviderLinux(); + final String result = await provider.getApplicationSupportPath(); _verifySampleFile(result, 'applicationSupport'); }); } -/// Verify a file called [name] in [directory] by recreating it with test +/// Verify a file called [name] in [directoryPath] by recreating it with test /// contents when necessary. -void _verifySampleFile(Directory directory, String name) { - final File file = File('${directory.path}/$name'); +void _verifySampleFile(String directoryPath, String name) { + final Directory directory = Directory(directoryPath); + final File file = File('${directory.path}${Platform.pathSeparator}$name'); if (file.existsSync()) { file.deleteSync(); diff --git a/packages/path_provider/path_provider_linux/example/lib/main.dart b/packages/path_provider/path_provider_linux/example/lib/main.dart index edba63482ad5..6958ed10cb23 100644 --- a/packages/path_provider/path_provider_linux/example/lib/main.dart +++ b/packages/path_provider/path_provider_linux/example/lib/main.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'dart:async'; import 'package:flutter/services.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:path_provider_linux/path_provider_linux.dart'; -void main() async { +void main() { runApp(MyApp()); } @@ -15,10 +15,11 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _tempDirectory = 'Unknown'; - String _downloadsDirectory = 'Unknown'; - String _appSupportDirectory = 'Unknown'; - String _documentsDirectory = 'Unknown'; + String? _tempDirectory = 'Unknown'; + String? _downloadsDirectory = 'Unknown'; + String? _appSupportDirectory = 'Unknown'; + String? _documentsDirectory = 'Unknown'; + final PathProviderLinux _provider = PathProviderLinux(); @override void initState() { @@ -28,32 +29,36 @@ class _MyAppState extends State { // Platform messages are asynchronous, so we initialize in an async method. Future initDirectories() async { - String tempDirectory; - String downloadsDirectory; - String appSupportDirectory; - String documentsDirectory; + String? tempDirectory; + String? downloadsDirectory; + String? appSupportDirectory; + String? documentsDirectory; // Platform messages may fail, so we use a try/catch PlatformException. try { - tempDirectory = (await getTemporaryDirectory()).path; - } on PlatformException { + tempDirectory = await _provider.getTemporaryPath(); + } on PlatformException catch (e, stackTrace) { tempDirectory = 'Failed to get temp directory.'; + print('$tempDirectory $e $stackTrace'); } try { - downloadsDirectory = (await getDownloadsDirectory()).path; - } on PlatformException { + downloadsDirectory = await _provider.getDownloadsPath(); + } on PlatformException catch (e, stackTrace) { downloadsDirectory = 'Failed to get downloads directory.'; + print('$downloadsDirectory $e $stackTrace'); } try { - documentsDirectory = (await getApplicationDocumentsDirectory()).path; - } on PlatformException { + documentsDirectory = await _provider.getApplicationDocumentsPath(); + } on PlatformException catch (e, stackTrace) { documentsDirectory = 'Failed to get documents directory.'; + print('$documentsDirectory $e $stackTrace'); } try { - appSupportDirectory = (await getApplicationSupportDirectory()).path; - } on PlatformException { + appSupportDirectory = await _provider.getApplicationSupportPath(); + } on PlatformException catch (e, stackTrace) { appSupportDirectory = 'Failed to get documents directory.'; + print('$appSupportDirectory $e $stackTrace'); } // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling diff --git a/packages/path_provider/path_provider_linux/example/pubspec.yaml b/packages/path_provider/path_provider_linux/example/pubspec.yaml index 85dbb24bbb29..cb778ef6ac57 100644 --- a/packages/path_provider/path_provider_linux/example/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/example/pubspec.yaml @@ -3,20 +3,19 @@ description: Demonstrates how to use the path_provider_linux plugin. publish_to: "none" environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - path_provider: ^1.6.10 - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.3 - -dependency_overrides: path_provider_linux: + # When depending on this package from a real application you should use: + # path_provider_linux: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -27,39 +26,5 @@ dev_dependencies: integration_test: path: ../../../integration_test -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - # To add custom fonts to your application, 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 - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/path_provider/path_provider_linux/example/test/widget_test.dart b/packages/path_provider/path_provider_linux/example/test/widget_test.dart index 8ebda3b77176..086b6d614e13 100644 --- a/packages/path_provider/path_provider_linux/example/test/widget_test.dart +++ b/packages/path_provider/path_provider_linux/example/test/widget_test.dart @@ -30,7 +30,7 @@ void main() { find.byWidgetPredicate( (Widget widget) => widget is Text && - widget.data.startsWith('Temp Directory: /tmp'), + widget.data!.startsWith('Temp Directory: /tmp'), ), findsOneWidget, ); @@ -48,7 +48,7 @@ void main() { find.byWidgetPredicate( (Widget widget) => widget is Text && - widget.data.startsWith('Documents Directory: /'), + widget.data!.startsWith('Documents Directory: /'), ), findsOneWidget, ); @@ -66,7 +66,7 @@ void main() { find.byWidgetPredicate( (Widget widget) => widget is Text && - widget.data.startsWith('Downloads Directory: /'), + widget.data!.startsWith('Downloads Directory: /'), ), findsOneWidget, ); @@ -85,7 +85,7 @@ void main() { find.byWidgetPredicate( (Widget widget) => widget is Text && - widget.data.startsWith('Application Support Directory: /'), + widget.data!.startsWith('Application Support Directory: /'), ), findsOneWidget, ); diff --git a/packages/path_provider/path_provider_linux/example/test_driver/integration_test.dart b/packages/path_provider/path_provider_linux/example/test_driver/integration_test.dart index 7a2c21338786..ac106b63b339 100644 --- a/packages/path_provider/path_provider_linux/example/test_driver/integration_test.dart +++ b/packages/path_provider/path_provider_linux/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/path_provider/path_provider_linux/ios/.gitignore b/packages/path_provider/path_provider_linux/ios/.gitignore deleted file mode 100644 index aa479fd3ce8a..000000000000 --- a/packages/path_provider/path_provider_linux/ios/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/path_provider/path_provider_linux/ios/path_provider_linux.podspec b/packages/path_provider/path_provider_linux/ios/path_provider_linux.podspec deleted file mode 100644 index 3649a30e67ef..000000000000 --- a/packages/path_provider/path_provider_linux/ios/path_provider_linux.podspec +++ /dev/null @@ -1,19 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'path_provider_linux' - s.version = '0.0.1' - s.summary = 'No-op implementation of path_provider linux plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of path_provider linux plugin - See https://github.com/flutter/flutter/issues/39659 - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_linux' } - s.documentation_url = 'https://pub.dev/packages/path_provider' - s.dependency 'Flutter' - s.platform = :ios, '8.0' -end \ No newline at end of file diff --git a/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart b/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart index 09d2447c0a6c..e35b73bf3766 100644 --- a/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart +++ b/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart @@ -18,12 +18,12 @@ class PathProviderLinux extends PathProviderPlatform { } @override - Future getTemporaryPath() { + Future getTemporaryPath() { return Future.value("/tmp"); } @override - Future getApplicationSupportPath() async { + Future getApplicationSupportPath() async { final processName = path.basenameWithoutExtension( await File('/proc/self/exe').resolveSymbolicLinks()); final directory = Directory(path.join(xdg.dataHome.path, processName)); @@ -35,12 +35,12 @@ class PathProviderLinux extends PathProviderPlatform { } @override - Future getApplicationDocumentsPath() { - return Future.value(xdg.getUserDirectory('DOCUMENTS').path); + Future getApplicationDocumentsPath() { + return Future.value(xdg.getUserDirectory('DOCUMENTS')?.path); } @override - Future getDownloadsPath() { - return Future.value(xdg.getUserDirectory('DOWNLOAD').path); + Future getDownloadsPath() { + return Future.value(xdg.getUserDirectory('DOWNLOAD')?.path); } } diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index feac1a6ec2f7..a3cdff34b6cc 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+1 +version: 2.0.0 homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_linux flutter: @@ -11,17 +11,17 @@ flutter: pluginClass: none environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" dependencies: - path: ^1.6.4 - xdg_directories: ^0.1.0 - path_provider_platform_interface: ^1.0.1 + path: ^1.8.0 + xdg_directories: ^0.2.0 + path_provider_platform_interface: ^2.0.0 flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 diff --git a/packages/path_provider/path_provider_macos/CHANGELOG.md b/packages/path_provider/path_provider_macos/CHANGELOG.md index ba2a38e563d6..f989efbe2a98 100644 --- a/packages/path_provider/path_provider_macos/CHANGELOG.md +++ b/packages/path_provider/path_provider_macos/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.0.0 + +* Update Dart SDK constraint for null safety compatibility. + +## 0.0.4+9 + +* Remove placeholder Dart file. + +## 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/README.md b/packages/path_provider/path_provider_macos/README.md index b97d9e81b7db..23727fe7f370 100644 --- a/packages/path_provider/path_provider_macos/README.md +++ b/packages/path_provider/path_provider_macos/README.md @@ -2,13 +2,6 @@ The macos implementation of [`path_provider`]. -**Please set your constraint to `path_provider_macos: '>=0.0.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. -Please use `path_provider_macos: '>=0.0.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package diff --git a/packages/path_provider/path_provider_macos/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_macos/example/integration_test/path_provider_test.dart index 58a4d1805cb0..1bb079051cfc 100644 --- a/packages/path_provider/path_provider_macos/example/integration_test/path_provider_test.dart +++ b/packages/path_provider/path_provider_macos/example/integration_test/path_provider_test.dart @@ -2,42 +2,56 @@ // 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:io'; import 'package:flutter_test/flutter_test.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('getTemporaryDirectory', (WidgetTester tester) async { - final Directory result = await getTemporaryDirectory(); + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String result = await provider.getTemporaryPath(); _verifySampleFile(result, 'temporaryDirectory'); }); testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { - final Directory result = await getApplicationDocumentsDirectory(); + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String result = await provider.getApplicationDocumentsPath(); _verifySampleFile(result, 'applicationDocuments'); }); testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { - final Directory result = await getApplicationSupportDirectory(); + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String result = await provider.getApplicationSupportPath(); _verifySampleFile(result, 'applicationSupport'); }); testWidgets('getLibraryDirectory', (WidgetTester tester) async { - if (!Platform.isMacOS) { - return; - } - final Directory result = await getLibraryDirectory(); + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String result = await provider.getLibraryPath(); _verifySampleFile(result, 'library'); }); + + testWidgets('getDownloadsDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String result = await provider.getDownloadsPath(); + // _verifySampleFile causes hangs in driver for some reason, so just + // validate that a non-empty path was returned. + expect(result, isNotEmpty); + }); } -/// Verify a file called [name] in [directory] by recreating it with test +/// Verify a file called [name] in [directoryPath] by recreating it with test /// contents when necessary. -void _verifySampleFile(Directory directory, String name) { - final File file = File('${directory.path}/$name'); +/// +/// If [createDirectory] is true, the directory will be created if missing. +void _verifySampleFile(String directoryPath, String name) { + final Directory directory = Directory(directoryPath); + final File file = File('${directory.path}${Platform.pathSeparator}$name'); if (file.existsSync()) { file.deleteSync(); diff --git a/packages/path_provider/path_provider_macos/example/ios/Flutter/AppFrameworkInfo.plist b/packages/path_provider/path_provider_macos/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Flutter/Debug.xcconfig b/packages/path_provider/path_provider_macos/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 9803018ca79d..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/path_provider/path_provider_macos/example/ios/Flutter/Release.xcconfig b/packages/path_provider/path_provider_macos/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index a4a8c604e13d..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index eb0222a7c9c5..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 2D9222481EC32A19007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D9222471EC32A19007564B0 /* 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, ); }; }; - 85DDFCF6BBDEE02B9D9F8138 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0EE60090AA5F3AAAF2175B6 /* 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 */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 2D9222461EC32A19007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 2D9222471EC32A19007564B0 /* 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 = ""; }; - 694A199F61914F41AAFD0B7F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 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 = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - D317CA1E83064E01753D8BB5 /* 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 = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 85DDFCF6BBDEE02B9D9F8138 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { - isa = PBXGroup; - children = ( - 694A199F61914F41AAFD0B7F /* Pods-Runner.debug.xcconfig */, - D317CA1E83064E01753D8BB5 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 840012C8B5EDBCF56B0E4AC1 /* Pods */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 2D9222461EC32A19007564B0 /* GeneratedPluginRegistrant.h */, - 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */, - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { - isa = PBXGroup; - children = ( - C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 2D9222481EC32A19007564B0 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.pathProviderExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.pathProviderExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/path_provider/path_provider_macos/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/AppDelegate.h b/packages/path_provider/path_provider_macos/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,10 +0,0 @@ -// 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 -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/AppDelegate.m b/packages/path_provider/path_provider_macos/example/ios/Runner/AppDelegate.m deleted file mode 100644 index a4b51c88eb60..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,16 +0,0 @@ -// 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. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/path_provider/path_provider_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index ebf48f603974..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Base.lproj/Main.storyboard b/packages/path_provider/path_provider_macos/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Info.plist b/packages/path_provider/path_provider_macos/example/ios/Runner/Info.plist deleted file mode 100644 index 342db6a5dcaf..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - path_provider_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/main.m b/packages/path_provider/path_provider_macos/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// 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 -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} 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..f19807f1c273 100644 --- a/packages/path_provider/path_provider_macos/example/lib/main.dart +++ b/packages/path_provider/path_provider_macos/example/lib/main.dart @@ -5,143 +5,87 @@ // ignore_for_file: public_member_api_docs import 'dart:async'; -import 'dart:io' show Directory; import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; void main() { runApp(MyApp()); } -class MyApp extends StatelessWidget { +/// Sample app +class MyApp extends StatefulWidget { @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Path Provider', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Path Provider'), - ); - } + _MyAppState createState() => _MyAppState(); } -class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); - final String title; +class _MyAppState extends State { + String? _tempDirectory = 'Unknown'; + String? _downloadsDirectory = 'Unknown'; + String? _appSupportDirectory = 'Unknown'; + String? _documentsDirectory = 'Unknown'; @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - Future _tempDirectory; - Future _appSupportDirectory; - Future _appDocumentsDirectory; - Future _appLibraryDirectory; - Future _downloadsDirectory; - - void _requestTempDirectory() { - setState(() { - _tempDirectory = getTemporaryDirectory(); - }); + void initState() { + super.initState(); + initDirectories(); } - Widget _buildDirectory( - BuildContext context, AsyncSnapshot snapshot) { - Text text = const Text(''); - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasError) { - text = Text('Error: ${snapshot.error}'); - } else if (snapshot.hasData) { - text = Text('path: ${snapshot.data.path}'); - } else { - text = const Text('path unavailable'); - } - } - return Padding(padding: const EdgeInsets.all(16.0), child: text); - } + // Platform messages are asynchronous, so we initialize in an async method. + Future initDirectories() async { + String? tempDirectory; + String? downloadsDirectory; + String? appSupportDirectory; + String? documentsDirectory; + final PathProviderPlatform provider = PathProviderPlatform.instance; - void _requestAppDocumentsDirectory() { - setState(() { - _appDocumentsDirectory = getApplicationDocumentsDirectory(); - }); - } + try { + tempDirectory = await provider.getTemporaryPath(); + } catch (exception) { + tempDirectory = 'Failed to get temp directory: $exception'; + } + try { + downloadsDirectory = await provider.getDownloadsPath(); + } catch (exception) { + downloadsDirectory = 'Failed to get downloads directory: $exception'; + } - void _requestAppSupportDirectory() { - setState(() { - _appSupportDirectory = getApplicationSupportDirectory(); - }); - } + try { + documentsDirectory = await provider.getApplicationDocumentsPath(); + } catch (exception) { + documentsDirectory = 'Failed to get documents directory: $exception'; + } - void _requestAppLibraryDirectory() { - setState(() { - _appLibraryDirectory = getLibraryDirectory(); - }); - } + try { + appSupportDirectory = await provider.getApplicationSupportPath(); + } catch (exception) { + appSupportDirectory = 'Failed to get app support directory: $exception'; + } - void _requestDownloadsDirectory() { setState(() { - _downloadsDirectory = getDownloadsDirectory(); + _tempDirectory = tempDirectory; + _downloadsDirectory = downloadsDirectory; + _appSupportDirectory = appSupportDirectory; + _documentsDirectory = documentsDirectory; }); } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Center( - child: ListView( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: const Text('Get Temporary Directory'), - onPressed: _requestTempDirectory, - ), - ), - FutureBuilder( - future: _tempDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: const Text('Get Application Documents Directory'), - onPressed: _requestAppDocumentsDirectory, - ), - ), - FutureBuilder( - future: _appDocumentsDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: const Text('Get Application Support Directory'), - onPressed: _requestAppSupportDirectory, - ), - ), - FutureBuilder( - future: _appSupportDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: const Text('Get Application Library Directory'), - onPressed: _requestAppLibraryDirectory, - ), - ), - FutureBuilder( - future: _appLibraryDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: const Text('Get Downlads Directory'), - onPressed: _requestDownloadsDirectory, - ), - ), - FutureBuilder( - future: _downloadsDirectory, builder: _buildDirectory), - ], + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Path Provider example app'), + ), + body: Center( + child: Column( + children: [ + Text('Temp Directory: $_tempDirectory\n'), + Text('Documents Directory: $_documentsDirectory\n'), + Text('Downloads Directory: $_downloadsDirectory\n'), + Text('Application Support Directory: $_appSupportDirectory\n'), + ], + ), ), ), ); diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/project.pbxproj index 1e39683e1446..51bfc333fa23 100644 --- a/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -27,10 +27,6 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -50,8 +46,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -72,14 +66,12 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 46139048DB9F59D473B61B5E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; F4586DA69948E3A954A2FC9C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -88,8 +80,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, 23F6FAA3AF82DFCF2B7DD79A /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -156,8 +146,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -281,7 +269,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -308,10 +296,13 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/path_provider_macos/path_provider_macos.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_macos.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/DebugProfile.entitlements b/packages/path_provider/path_provider_macos/example/macos/Runner/DebugProfile.entitlements index dddb8a30c851..8139952b3e55 100644 --- a/packages/path_provider/path_provider_macos/example/macos/Runner/DebugProfile.entitlements +++ b/packages/path_provider/path_provider_macos/example/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.files.downloads.read-write + diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Release.entitlements b/packages/path_provider/path_provider_macos/example/macos/Runner/Release.entitlements index 852fa1a4728a..2f9659c917fb 100644 --- a/packages/path_provider/path_provider_macos/example/macos/Runner/Release.entitlements +++ b/packages/path_provider/path_provider_macos/example/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.files.downloads.read-write + diff --git a/packages/path_provider/path_provider_macos/example/pubspec.yaml b/packages/path_provider/path_provider_macos/example/pubspec.yaml index 01ab42bbe71b..db7fd9a0dea6 100644 --- a/packages/path_provider/path_provider_macos/example/pubspec.yaml +++ b/packages/path_provider/path_provider_macos/example/pubspec.yaml @@ -4,13 +4,14 @@ description: Demonstrates how to use the path_provider plugin. dependencies: flutter: sdk: flutter - path_provider: ^1.6.14 - -# path_provider_macos is endorsed, so we need to add it to dependency_overrides -# to depend on it from path: -dependency_overrides: path_provider_macos: + # When depending on this package from a real application you should use: + # path_provider_macos: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ + path_provider_platform_interface: ^2.0.0 dev_dependencies: integration_test: @@ -23,5 +24,5 @@ flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/path_provider/path_provider_macos/example/test_driver/integration_test.dart b/packages/path_provider/path_provider_macos/example/test_driver/integration_test.dart index 7a2c21338786..ac106b63b339 100644 --- a/packages/path_provider/path_provider_macos/example/test_driver/integration_test.dart +++ b/packages/path_provider/path_provider_macos/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/path_provider/path_provider_macos/ios/path_provider_macos.podspec b/packages/path_provider/path_provider_macos/ios/path_provider_macos.podspec deleted file mode 100644 index 9f822c58c45c..000000000000 --- a/packages/path_provider/path_provider_macos/ios/path_provider_macos.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'path_provider_macos' - s.version = '0.0.1' - s.summary = 'No-op implementation of path_provider macOS plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of path_provider macOS plugin - See https://github.com/flutter/flutter/issues/39659 - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_macos' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end - diff --git a/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart b/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart deleted file mode 100644 index cf440b2858af..000000000000 --- a/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart +++ /dev/null @@ -1,3 +0,0 @@ -// Analyze will fail if there is no main.dart file. This file should -// be removed once an example app has been added to path_provider_macos. -// https://github.com/flutter/flutter/issues/51007 diff --git a/packages/path_provider/path_provider_macos/pubspec.yaml b/packages/path_provider/path_provider_macos/pubspec.yaml index 69241491d29d..14f0b9556a6a 100644 --- a/packages/path_provider/path_provider_macos/pubspec.yaml +++ b/packages/path_provider/path_provider_macos/pubspec.yaml @@ -1,9 +1,6 @@ name: path_provider_macos 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: 2.0.0 homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_macos flutter: @@ -13,12 +10,12 @@ flutter: pluginClass: PathProviderPlugin environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter dev_dependencies: - pedantic: ^1.8.0 + pedantic: ^1.10.0 diff --git a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md index 744764c3215a..08dc9f69c7df 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 + +* 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..3feb4e0a8ebd 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 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 + platform: ^3.0.0 + plugin_platform_interface: ">=1.0.0 <3.0.0" dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.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..ca4621471891 100644 --- a/packages/path_provider/path_provider_windows/CHANGELOG.md +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.0 + +* Migrate to null safety + +## 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/README.md b/packages/path_provider/path_provider_windows/README.md index 66a05f9e7347..6d452e770469 100644 --- a/packages/path_provider/path_provider_windows/README.md +++ b/packages/path_provider/path_provider_windows/README.md @@ -2,14 +2,6 @@ The Windows implementation of [`path_provider`][1]. -**Please set your constraint to `path_provider_windows: '>=0.0.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming - -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. -Please use `path_provider_windows: '>=0.0.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package diff --git a/packages/path_provider/path_provider_windows/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_windows/example/integration_test/path_provider_test.dart index ee9427686026..0953fc100950 100644 --- a/packages/path_provider/path_provider_windows/example/integration_test/path_provider_test.dart +++ b/packages/path_provider/path_provider_windows/example/integration_test/path_provider_test.dart @@ -2,13 +2,15 @@ // 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:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:path_provider_windows/path_provider_windows.dart'; -import 'package:e2e/e2e.dart'; +import 'package:integration_test/integration_test.dart'; void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('getTemporaryDirectory', (WidgetTester tester) async { final PathProviderWindows provider = PathProviderWindows(); diff --git a/packages/path_provider/path_provider_windows/example/lib/main.dart b/packages/path_provider/path_provider_windows/example/lib/main.dart index 4fbb1decf2f4..964ef3d04db1 100644 --- a/packages/path_provider/path_provider_windows/example/lib/main.dart +++ b/packages/path_provider/path_provider_windows/example/lib/main.dart @@ -9,7 +9,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:path_provider_windows/path_provider_windows.dart'; -void main() async { +void main() { runApp(MyApp()); } @@ -20,10 +20,10 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _tempDirectory = 'Unknown'; - String _downloadsDirectory = 'Unknown'; - String _appSupportDirectory = 'Unknown'; - String _documentsDirectory = 'Unknown'; + String? _tempDirectory = 'Unknown'; + String? _downloadsDirectory = 'Unknown'; + String? _appSupportDirectory = 'Unknown'; + String? _documentsDirectory = 'Unknown'; @override void initState() { @@ -33,10 +33,10 @@ class _MyAppState extends State { // Platform messages are asynchronous, so we initialize in an async method. Future initDirectories() async { - String tempDirectory; - String downloadsDirectory; - String appSupportDirectory; - String documentsDirectory; + String? tempDirectory; + String? downloadsDirectory; + String? appSupportDirectory; + String? documentsDirectory; final PathProviderWindows provider = PathProviderWindows(); try { diff --git a/packages/path_provider/path_provider_windows/example/pubspec.yaml b/packages/path_provider/path_provider_windows/example/pubspec.yaml index 0e723f502581..8c1f88b89cb0 100644 --- a/packages/path_provider/path_provider_windows/example/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/example/pubspec.yaml @@ -4,21 +4,24 @@ description: Demonstrates how to use the path_provider plugin. dependencies: flutter: sdk: flutter - path_provider_windows: any - -dependency_overrides: path_provider_windows: + # When depending on this package from a real application you should use: + # path_provider_windows: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: - e2e: ^0.2.1 + integration_test: + path: ../../../integration_test flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/path_provider/path_provider_windows/example/test_driver/integration_test.dart b/packages/path_provider/path_provider_windows/example/test_driver/integration_test.dart index f3aa9e218d82..ac106b63b339 100644 --- a/packages/path_provider/path_provider_windows/example/test_driver/integration_test.dart +++ b/packages/path_provider/path_provider_windows/example/test_driver/integration_test.dart @@ -2,14 +2,18 @@ // 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'; import 'package:flutter_driver/flutter_driver.dart'; Future main() async { final FlutterDriver driver = await FlutterDriver.connect(); - final String result = + final String data = await driver.requestData(null, timeout: const Duration(minutes: 1)); await driver.close(); - exit(result == 'pass' ? 0 : 1); + final Map result = jsonDecode(data); + exit(result['result'] == 'true' ? 0 : 1); } diff --git a/packages/path_provider/path_provider_windows/example/windows/flutter/CMakeLists.txt b/packages/path_provider/path_provider_windows/example/windows/flutter/CMakeLists.txt index c7a8c7607d81..744f08a9389b 100644 --- a/packages/path_provider/path_provider_windows/example/windows/flutter/CMakeLists.txt +++ b/packages/path_provider/path_provider_windows/example/windows/flutter/CMakeLists.txt @@ -91,6 +91,7 @@ add_custom_command( ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" windows-x64 $ + VERBATIM ) add_custom_target(flutter_assemble DEPENDS "${FLUTTER_LIBRARY}" diff --git a/packages/path_provider/path_provider_windows/ios/path_provider_windows.podspec b/packages/path_provider/path_provider_windows/ios/path_provider_windows.podspec deleted file mode 100644 index 941a36c1c794..000000000000 --- a/packages/path_provider/path_provider_windows/ios/path_provider_windows.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# Run `pod lib lint path_provider_windows.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'path_provider_windows' - s.version = '0.0.1' - s.summary = 'path_provider_windows iOS stub' - s.description = <<-DESC - No-op implementation of the windows path_provider plugin to avoid build issues on iOS - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_windows' } - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end 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..db2ad9da207c 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 @@ -22,24 +22,23 @@ import 'folders.dart'; class VersionInfoQuerier { /// Returns the value for [key] in [versionInfo]s English strings section, or /// null if there is no such entry, or if versionInfo is null. - getStringValue(Pointer versionInfo, key) { + getStringValue(Pointer? versionInfo, key) { if (versionInfo == null) { return null; } const kEnUsLanguageCode = '040904e4'; final keyPath = TEXT('\\StringFileInfo\\$kEnUsLanguageCode\\$key'); - final length = allocate(); - final valueAddress = allocate(); + final length = calloc(); + final valueAddress = calloc>(); try { if (VerQueryValue(versionInfo, keyPath, valueAddress, length) == 0) { return null; } - return Pointer.fromAddress(valueAddress.value) - .unpackString(length.value); + return valueAddress.value.toDartString(); } finally { - free(keyPath); - free(length); - free(valueAddress); + calloc.free(keyPath); + calloc.free(length); + calloc.free(valueAddress); } } } @@ -54,8 +53,8 @@ class PathProviderWindows extends PathProviderPlatform { /// This is typically the same as the TMP environment variable. @override - Future getTemporaryPath() async { - final buffer = allocate(count: MAX_PATH + 1).cast(); + Future getTemporaryPath() async { + final buffer = calloc(MAX_PATH + 1).cast(); String path; try { @@ -65,7 +64,7 @@ class PathProviderWindows extends PathProviderPlatform { final error = GetLastError(); throw WindowsException(error); } else { - path = buffer.unpackString(length); + path = buffer.toDartString(); // GetTempPath adds a trailing backslash, but SHGetKnownFolderPath does // not. Strip off trailing backslash for consistency with other methods @@ -83,12 +82,12 @@ class PathProviderWindows extends PathProviderPlatform { return Future.value(path); } finally { - free(buffer); + calloc.free(buffer); } } @override - Future getApplicationSupportPath() async { + Future getApplicationSupportPath() async { final appDataRoot = await getPath(WindowsKnownFolder.RoamingAppData); final directory = Directory( path.join(appDataRoot, _getApplicationSpecificSubdirectory())); @@ -105,25 +104,27 @@ class PathProviderWindows extends PathProviderPlatform { } @override - Future getApplicationDocumentsPath() => + Future getApplicationDocumentsPath() => getPath(WindowsKnownFolder.Documents); @override - Future getDownloadsPath() => getPath(WindowsKnownFolder.Downloads); + Future getDownloadsPath() => getPath(WindowsKnownFolder.Downloads); /// Retrieve any known folder from Windows. /// /// folderID is a GUID that represents a specific known folder ID, drawn from /// [WindowsKnownFolder]. Future getPath(String folderID) { - final pathPtrPtr = allocate(); - Pointer pathPtr; + final pathPtrPtr = calloc>(); + final Pointer knownFolderID = calloc()..ref.setGUID(folderID); try { - GUID knownFolderID = GUID.fromString(folderID); - final hr = SHGetKnownFolderPath( - knownFolderID.addressOf, KF_FLAG_DEFAULT, NULL, pathPtrPtr); + knownFolderID, + KF_FLAG_DEFAULT, + NULL, + pathPtrPtr, + ); if (FAILED(hr)) { if (hr == E_INVALIDARG || hr == E_FAIL) { @@ -131,12 +132,11 @@ class PathProviderWindows extends PathProviderPlatform { } } - pathPtr = Pointer.fromAddress(pathPtrPtr.value); - final path = pathPtr.unpackString(MAX_PATH); + final path = pathPtrPtr.value.toDartString(); return Future.value(path); } finally { - CoTaskMemFree(pathPtr.cast()); - free(pathPtrPtr); + calloc.free(pathPtrPtr); + calloc.free(knownFolderID); } } @@ -151,13 +151,13 @@ class PathProviderWindows extends PathProviderPlatform { /// - If the product name isn't there, it will use the exe's filename (without /// extension). String _getApplicationSpecificSubdirectory() { - String companyName; - String productName; + String? companyName; + String? productName; final Pointer moduleNameBuffer = - allocate(count: MAX_PATH + 1).cast(); - final Pointer unused = allocate(); - Pointer infoBuffer; + calloc(MAX_PATH + 1).cast(); + final Pointer unused = calloc(); + Pointer? infoBuffer; try { // Get the module name. final moduleNameLength = GetModuleFileName(0, moduleNameBuffer, MAX_PATH); @@ -169,10 +169,10 @@ class PathProviderWindows extends PathProviderPlatform { // From that, load the VERSIONINFO resource int infoSize = GetFileVersionInfoSize(moduleNameBuffer, unused); if (infoSize != 0) { - infoBuffer = allocate(count: infoSize); + infoBuffer = calloc(infoSize); if (GetFileVersionInfo(moduleNameBuffer, 0, infoSize, infoBuffer) == 0) { - free(infoBuffer); + calloc.free(infoBuffer); infoBuffer = null; } } @@ -183,18 +183,18 @@ class PathProviderWindows extends PathProviderPlatform { // If there was no product name, use the executable name. if (productName == null) { - productName = path.basenameWithoutExtension( - moduleNameBuffer.unpackString(moduleNameLength)); + productName = + path.basenameWithoutExtension(moduleNameBuffer.toDartString()); } return companyName != null ? path.join(companyName, productName) : productName; } finally { - free(moduleNameBuffer); - free(unused); + calloc.free(moduleNameBuffer); + calloc.free(unused); if (infoBuffer != null) { - free(infoBuffer); + calloc.free(infoBuffer); } } } @@ -203,7 +203,7 @@ class PathProviderWindows extends PathProviderPlatform { /// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions /// /// If after sanitizing the string is empty, returns null. - String _sanitizedDirectoryName(String rawString) { + String? _sanitizedDirectoryName(String? rawString) { if (rawString == null) { return null; } diff --git a/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_stub.dart b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_stub.dart index 11a946542f26..1a0e84e8f0da 100644 --- a/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_stub.dart +++ b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_stub.dart @@ -19,7 +19,7 @@ class PathProviderWindows extends PathProviderPlatform { } /// Stub; see comment on VersionInfoQuerier. - VersionInfoQuerier versionInfoQuerier; + VersionInfoQuerier versionInfoQuerier = VersionInfoQuerier(); /// Match PathProviderWindows so that the analyzer won't report invalid /// overrides if tests provide fake PathProviderWindows implementations. diff --git a/packages/path_provider/path_provider_windows/pubspec.yaml b/packages/path_provider/path_provider_windows/pubspec.yaml index 342774680dc4..d152869f197d 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: 2.0.0 flutter: plugin: @@ -11,19 +11,19 @@ flutter: pluginClass: none dependencies: - path_provider_platform_interface: ^1.0.3 - meta: ^1.0.5 - path: ^1.6.4 + path_provider_platform_interface: ^2.0.0 + meta: ^1.3.0 + path: ^1.8.0 flutter: sdk: flutter - ffi: ^0.1.3 - win32: ^1.7.1 + ffi: ^1.0.0 + win32: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart b/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart index 83ceea9cdf0c..989f3673ac63 100644 --- a/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart +++ b/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart @@ -13,7 +13,7 @@ class FakeVersionInfoQuerier implements VersionInfoQuerier { final Map responses; - getStringValue(Pointer versionInfo, key) => responses[key]; + getStringValue(Pointer? versionInfo, key) => responses[key]; } void main() { @@ -40,8 +40,11 @@ void main() { 'ProductName': 'Amazing App', }); final path = await pathProvider.getApplicationSupportPath(); - expect(path, endsWith(r'AppData\Roaming\A Company\Amazing App')); - expect(Directory(path).existsSync(), isTrue); + expect(path, isNotNull); + if (path != null) { + expect(path, endsWith(r'AppData\Roaming\A Company\Amazing App')); + expect(Directory(path).existsSync(), isTrue); + } }, skip: !Platform.isWindows); test('getApplicationSupportPath with missing company', () async { @@ -50,8 +53,11 @@ void main() { 'ProductName': 'Amazing App', }); final path = await pathProvider.getApplicationSupportPath(); - expect(path, endsWith(r'AppData\Roaming\Amazing App')); - expect(Directory(path).existsSync(), isTrue); + expect(path, isNotNull); + if (path != null) { + expect(path, endsWith(r'AppData\Roaming\Amazing App')); + expect(Directory(path).existsSync(), isTrue); + } }, skip: !Platform.isWindows); test('getApplicationSupportPath with problematic values', () async { @@ -61,12 +67,15 @@ void main() { 'ProductName': r'A"/Terrible\|App?*Name', }); final path = await pathProvider.getApplicationSupportPath(); - expect( - path, - endsWith(r'AppData\Roaming\' - r'A _Bad_ Company_ Name\' - r'A__Terrible__App__Name')); - expect(Directory(path).existsSync(), isTrue); + expect(path, isNotNull); + if (path != null) { + expect( + path, + endsWith(r'AppData\Roaming\' + r'A _Bad_ Company_ Name\' + r'A__Terrible__App__Name')); + expect(Directory(path).existsSync(), isTrue); + } }, skip: !Platform.isWindows); test('getApplicationSupportPath with a completely invalid company', () async { @@ -76,8 +85,11 @@ void main() { 'ProductName': r'Amazing App', }); final path = await pathProvider.getApplicationSupportPath(); - expect(path, endsWith(r'AppData\Roaming\Amazing App')); - expect(Directory(path).existsSync(), isTrue); + expect(path, isNotNull); + if (path != null) { + expect(path, endsWith(r'AppData\Roaming\Amazing App')); + expect(Directory(path).existsSync(), isTrue); + } }, skip: !Platform.isWindows); test('getApplicationSupportPath with very long app name', () async { diff --git a/packages/plugin_platform_interface/CHANGELOG.md b/packages/plugin_platform_interface/CHANGELOG.md index 01b5ff7d1252..cea8f2a76266 100644 --- a/packages/plugin_platform_interface/CHANGELOG.md +++ b/packages/plugin_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.0 + +* 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..6e425a19a048 100644 --- a/packages/plugin_platform_interface/lib/plugin_platform_interface.dart +++ b/packages/plugin_platform_interface/lib/plugin_platform_interface.dart @@ -41,9 +41,9 @@ 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; + final Object? _instanceToken; /// Ensures that the platform instance has a token that matches the /// provided token and throws [AssertionError] if not. diff --git a/packages/plugin_platform_interface/pubspec.yaml b/packages/plugin_platform_interface/pubspec.yaml index ae11b144ca8c..12a5b066bc61 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: 2.0.0 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 dev_dependencies: - mockito: ^4.1.1 - test: ^1.9.4 - pedantic: ^1.8.0 + mockito: ^5.0.0-nullsafety.7 + test: ^1.16.0 + pedantic: ^1.10.0 diff --git a/packages/quick_actions/CHANGELOG.md b/packages/quick_actions/CHANGELOG.md index 0742d73f500a..276ddfbf24c4 100644 --- a/packages/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/CHANGELOG.md @@ -1,3 +1,16 @@ +## 0.5.0 + +* Migrate to null safety. +* Fixes quick actions not working on iOS. + +## 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/example/integration_test/quick_actions_test.dart b/packages/quick_actions/example/integration_test/quick_actions_test.dart index 03ecfe491d93..43822c9e8b2b 100644 --- a/packages/quick_actions/example/integration_test/quick_actions_test.dart +++ b/packages/quick_actions/example/integration_test/quick_actions_test.dart @@ -2,6 +2,7 @@ // 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:quick_actions/quick_actions.dart'; diff --git a/packages/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj b/packages/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj index fdd275fcede5..dba32819ce42 100644 --- a/packages/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,11 +9,8 @@ /* 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, ); }; }; + 686BE83025E58CCF00862533 /* RunnerUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686BE82F25E58CCF00862533 /* RunnerUITests.m */; }; 83C36CAF23D629E5ABE75B2A /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CCC799F2B0AB50A9C34344F0 /* 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 */; }; @@ -21,6 +18,16 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 686BE83225E58CCF00862533 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -28,8 +35,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -40,14 +45,15 @@ 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 = ""; }; 5278439583922091276A37C9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 686BE82F25E58CCF00862533 /* RunnerUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerUITests.m; sourceTree = ""; }; + 686BE83125E58CCF00862533 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 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 = ""; }; @@ -59,12 +65,17 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 686BE82A25E58CCF00862533 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 83C36CAF23D629E5ABE75B2A /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -72,12 +83,19 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 686BE82E25E58CCF00862533 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + 686BE82F25E58CCF00862533 /* RunnerUITests.m */, + 686BE83125E58CCF00862533 /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -90,6 +108,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + 686BE82E25E58CCF00862533 /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, D0FE95BE2380323DD75CB891 /* Pods */, A44AD0D63DEF785A2A2DEE28 /* Frameworks */, @@ -100,6 +119,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */, ); name = Products; sourceTree = ""; @@ -148,6 +168,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 686BE82C25E58CCF00862533 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 686BE83625E58CCF00862533 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + 686BE82925E58CCF00862533 /* Sources */, + 686BE82A25E58CCF00862533 /* Frameworks */, + 686BE82B25E58CCF00862533 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 686BE83325E58CCF00862533 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -159,7 +197,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - FEDDF02AA7C2BA0D1905BD95 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -179,6 +216,11 @@ LastUpgradeCheck = 1100; ORGANIZATIONNAME = "The Chromium Authors"; TargetAttributes = { + 686BE82C25E58CCF00862533 = { + CreatedOnToolsVersion = 12.4; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; @@ -198,11 +240,19 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 686BE82C25E58CCF00862533 /* RunnerUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 686BE82B25E58CCF00862533 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -229,7 +279,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"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -263,24 +313,17 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - FEDDF02AA7C2BA0D1905BD95 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 686BE82925E58CCF00862533 /* Sources */ = { + isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( + 686BE83025E58CCF00862533 /* RunnerUITests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -293,6 +336,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 686BE83325E58CCF00862533 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 686BE83225E58CCF00862533 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -313,9 +364,53 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 686BE83425E58CCF00862533 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + 686BE83525E58CCF00862533 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -372,7 +467,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -466,6 +560,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 686BE83625E58CCF00862533 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 686BE83425E58CCF00862533 /* Debug */, + 686BE83525E58CCF00862533 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/quick_actions/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/quick_actions/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/quick_actions/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3bb3697ef41c..9850cc113026 100644 --- a/packages/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -37,6 +37,16 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/quick_actions/example/ios/RunnerUITests/Info.plist b/packages/quick_actions/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/quick_actions/example/ios/RunnerUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/quick_actions/example/ios/RunnerUITests/RunnerUITests.m b/packages/quick_actions/example/ios/RunnerUITests/RunnerUITests.m new file mode 100644 index 000000000000..f78081b98a01 --- /dev/null +++ b/packages/quick_actions/example/ios/RunnerUITests/RunnerUITests.m @@ -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 +#import + +static const int kElementWaitingTime = 30; + +@interface RunnerUITests : XCTestCase + +@end + +@implementation RunnerUITests + +- (void)setUp { + [super setUp]; + self.continueAfterFailure = NO; +} + +- (void)testQuickAction { + XCUIApplication *app = [[XCUIApplication alloc] init]; + [app launch]; + XCUIElement *actionsReady = app.otherElements[@"actions ready"]; + if (![actionsReady waitForExistenceWithTimeout:kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find the actionsReady in the app with %@ seconds", + @(kElementWaitingTime)); + } + + [[XCUIDevice sharedDevice] pressButton:XCUIDeviceButtonHome]; + + XCUIApplication *springboard = + [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"]; + XCUIElement *quickActionsAppIcon = springboard.icons[@"quick_actions_example"]; + if (![quickActionsAppIcon waitForExistenceWithTimeout:kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", springboard.debugDescription); + XCTFail(@"Failed due to not able to find the example app from springboard with %@ seconds", + @(kElementWaitingTime)); + } + + [quickActionsAppIcon pressForDuration:2]; + XCUIElement *actionOne = springboard.buttons[@"Action one"]; + if (![actionOne waitForExistenceWithTimeout:kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", springboard.debugDescription); + XCTFail(@"Failed due to not able to find the actionOne button from springboard with %@ seconds", + @(kElementWaitingTime)); + } + + [actionOne tap]; + + XCUIElement *actionOneConfirmation = app.otherElements[@"action_one"]; + if (![actionOneConfirmation waitForExistenceWithTimeout:kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", springboard.debugDescription); + XCTFail(@"Failed due to not able to find the actionOneConfirmation in the app with %@ seconds", + @(kElementWaitingTime)); + } + XCTAssertTrue(actionOneConfirmation.exists); +} + +@end diff --git a/packages/quick_actions/example/lib/main.dart b/packages/quick_actions/example/lib/main.dart index fc289810ea24..08d8f4a1fbce 100644 --- a/packages/quick_actions/example/lib/main.dart +++ b/packages/quick_actions/example/lib/main.dart @@ -25,7 +25,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key}) : super(key: key); + MyHomePage({Key? key}) : super(key: key); @override _MyHomePageState createState() => _MyHomePageState(); @@ -41,7 +41,9 @@ class _MyHomePageState extends State { final QuickActions quickActions = QuickActions(); quickActions.initialize((String shortcutType) { setState(() { - if (shortcutType != null) shortcut = shortcutType; + if (shortcutType != null) { + shortcut = shortcutType; + } }); }); @@ -59,7 +61,11 @@ class _MyHomePageState extends State { type: 'action_two', localizedTitle: 'Action two', icon: 'ic_launcher'), - ]); + ]).then((value) { + setState(() { + shortcut = "actions ready"; + }); + }); } @override diff --git a/packages/quick_actions/example/pubspec.yaml b/packages/quick_actions/example/pubspec.yaml index 6f9c0efd01af..ded88685c41b 100644 --- a/packages/quick_actions/example/pubspec.yaml +++ b/packages/quick_actions/example/pubspec.yaml @@ -5,6 +5,11 @@ dependencies: flutter: sdk: flutter quick_actions: + # When depending on this package from a real application you should use: + # quick_actions: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -12,11 +17,11 @@ dev_dependencies: sdk: flutter integration_test: path: ../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.9.1+hotfix.2" diff --git a/packages/quick_actions/example/test_driver/integration_test.dart b/packages/quick_actions/example/test_driver/integration_test.dart index 7a2c21338786..0352d4aaeb2d 100644 --- a/packages/quick_actions/example/test_driver/integration_test.dart +++ b/packages/quick_actions/example/test_driver/integration_test.dart @@ -2,6 +2,7 @@ // 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/quick_actions/ios/Classes/FLTQuickActionsPlugin.m b/packages/quick_actions/ios/Classes/FLTQuickActionsPlugin.m index 88ff7397af8a..c99c016ed1ed 100644 --- a/packages/quick_actions/ios/Classes/FLTQuickActionsPlugin.m +++ b/packages/quick_actions/ios/Classes/FLTQuickActionsPlugin.m @@ -19,6 +19,7 @@ + (void)registerWithRegistrar:(NSObject *)registrar { FLTQuickActionsPlugin *instance = [[FLTQuickActionsPlugin alloc] init]; instance.channel = channel; [registrar addMethodCallDelegate:instance channel:channel]; + [registrar addApplicationDelegate:instance]; } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { diff --git a/packages/quick_actions/lib/quick_actions.dart b/packages/quick_actions/lib/quick_actions.dart index 933162a1a47c..875661244a74 100644 --- a/packages/quick_actions/lib/quick_actions.dart +++ b/packages/quick_actions/lib/quick_actions.dart @@ -22,8 +22,8 @@ class ShortcutItem { /// /// Only [icon] should be nullable. It will remain `null` if unset. const ShortcutItem({ - @required this.type, - @required this.localizedTitle, + required this.type, + required this.localizedTitle, this.icon, }); @@ -35,7 +35,7 @@ class ShortcutItem { /// Name of native resource (xcassets etc; NOT a Flutter asset) to be /// displayed as the icon for this item. - final String icon; + final String? icon; } /// Quick actions plugin. @@ -65,7 +65,8 @@ class QuickActions { assert(call.method == 'launch'); handler(call.arguments); }); - final String action = await channel.invokeMethod('getLaunchAction'); + final String? action = + await channel.invokeMethod('getLaunchAction'); if (action != null) { handler(action); } @@ -73,7 +74,7 @@ class QuickActions { /// Sets the [ShortcutItem]s to become the app's quick actions. Future setShortcutItems(List items) async { - final List> itemsList = + final List> itemsList = items.map(_serializeItem).toList(); await channel.invokeMethod('setShortcutItems', itemsList); } @@ -82,8 +83,8 @@ class QuickActions { Future clearShortcutItems() => channel.invokeMethod('clearShortcutItems'); - Map _serializeItem(ShortcutItem item) { - return { + Map _serializeItem(ShortcutItem item) { + return { 'type': item.type, 'localizedTitle': item.localizedTitle, 'icon': item.icon, diff --git a/packages/quick_actions/pubspec.yaml b/packages/quick_actions/pubspec.yaml index 187c5707ddea..dc500e2516e1 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.5.0 flutter: plugin: @@ -16,17 +16,16 @@ flutter: dependencies: flutter: sdk: flutter - meta: ^1.0.5 + meta: ^1.3.0 dev_dependencies: - test: ^1.3.0 - mockito: ^3.0.0 + test: ^1.16.3 flutter_test: sdk: flutter integration_test: path: ../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/quick_actions/test/quick_actions_test.dart b/packages/quick_actions/test/quick_actions_test.dart index ffb6de1024fd..8066719e5113 100644 --- a/packages/quick_actions/test/quick_actions_test.dart +++ b/packages/quick_actions/test/quick_actions_test.dart @@ -10,7 +10,7 @@ import 'package:quick_actions/quick_actions.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - QuickActions quickActions; + late QuickActions quickActions; final List log = []; setUp(() { diff --git a/packages/sensors/CHANGELOG.md b/packages/sensors/CHANGELOG.md index a2693dd7ed0b..295c32ad2127 100644 --- a/packages/sensors/CHANGELOG.md +++ b/packages/sensors/CHANGELOG.md @@ -1,3 +1,15 @@ +## 2.0.0 + +* Migrate to null safety. + +## 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..8c5ab008419b 100644 --- a/packages/sensors/README.md +++ b/packages/sensors/README.md @@ -1,19 +1,12 @@ # sensors -**Please set your constraint to `sensors: '>=0.4.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The sensors plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.4.y+z`. -Please use `sensors: '>=0.4.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - 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/example/lib/main.dart b/packages/sensors/example/lib/main.dart index 575e0493742f..d6f01380c534 100644 --- a/packages/sensors/example/lib/main.dart +++ b/packages/sensors/example/lib/main.dart @@ -28,7 +28,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; @@ -41,21 +41,21 @@ class _MyHomePageState extends State { static const int _snakeColumns = 20; static const double _snakeCellSize = 10.0; - List _accelerometerValues; - List _userAccelerometerValues; - List _gyroscopeValues; + List? _accelerometerValues; + List? _userAccelerometerValues; + List? _gyroscopeValues; List> _streamSubscriptions = >[]; @override Widget build(BuildContext context) { - final List accelerometer = - _accelerometerValues?.map((double v) => v.toStringAsFixed(1))?.toList(); - final List gyroscope = - _gyroscopeValues?.map((double v) => v.toStringAsFixed(1))?.toList(); - final List userAccelerometer = _userAccelerometerValues + final List? accelerometer = + _accelerometerValues?.map((double v) => v.toStringAsFixed(1)).toList(); + final List? gyroscope = + _gyroscopeValues?.map((double v) => v.toStringAsFixed(1)).toList(); + final List? userAccelerometer = _userAccelerometerValues ?.map((double v) => v.toStringAsFixed(1)) - ?.toList(); + .toList(); return Scaffold( appBar: AppBar( diff --git a/packages/sensors/example/lib/snake.dart b/packages/sensors/example/lib/snake.dart index d6b2f9b48a23..72f27472dd5b 100644 --- a/packages/sensors/example/lib/snake.dart +++ b/packages/sensors/example/lib/snake.dart @@ -56,15 +56,14 @@ class SnakeBoardPainter extends CustomPainter { } class SnakeState extends State { - SnakeState(int rows, int columns, this.cellSize) { - state = GameState(rows, columns); - } + SnakeState(int rows, int columns, this.cellSize) + : state = GameState(rows, columns); double cellSize; GameState state; - AccelerometerEvent acceleration; - StreamSubscription _streamSubscription; - Timer _timer; + AccelerometerEvent? acceleration; + late StreamSubscription _streamSubscription; + late Timer _timer; @override Widget build(BuildContext context) { @@ -96,21 +95,21 @@ class SnakeState extends State { } void _step() { - final math.Point newDirection = acceleration == null + final AccelerometerEvent? currentAcceleration = acceleration; + final math.Point? newDirection = currentAcceleration == null ? null - : acceleration.x.abs() < 1.0 && acceleration.y.abs() < 1.0 + : currentAcceleration.x.abs() < 1.0 && currentAcceleration.y.abs() < 1.0 ? null - : (acceleration.x.abs() < acceleration.y.abs()) - ? math.Point(0, acceleration.y.sign.toInt()) - : math.Point(-acceleration.x.sign.toInt(), 0); + : (currentAcceleration.x.abs() < currentAcceleration.y.abs()) + ? math.Point(0, currentAcceleration.y.sign.toInt()) + : math.Point(-currentAcceleration.x.sign.toInt(), 0); state.step(newDirection); } } class GameState { - GameState(this.rows, this.columns) { - snakeLength = math.min(rows, columns) - 5; - } + GameState(this.rows, this.columns) + : snakeLength = math.min(rows, columns) - 5; int rows; int columns; @@ -119,7 +118,7 @@ class GameState { List> body = >[const math.Point(0, 0)]; math.Point direction = const math.Point(1, 0); - void step(math.Point newDirection) { + void step(math.Point? newDirection) { math.Point next = body.last + direction; next = math.Point(next.x % columns, next.y % rows); diff --git a/packages/sensors/example/pubspec.yaml b/packages/sensors/example/pubspec.yaml index eb46c611a43a..0cd30b12df2b 100644 --- a/packages/sensors/example/pubspec.yaml +++ b/packages/sensors/example/pubspec.yaml @@ -5,6 +5,11 @@ dependencies: flutter: sdk: flutter sensors: + # When depending on this package from a real application you should use: + # sensors: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -12,12 +17,11 @@ dev_dependencies: sdk: flutter integration_test: path: ../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2 <2.0.0" - + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/sensors/example/test_driver/test/integration_test.dart b/packages/sensors/example/test_driver/test/integration_test.dart index 7a2c21338786..a8a56aa90f6a 100644 --- a/packages/sensors/example/test_driver/test/integration_test.dart +++ b/packages/sensors/example/test_driver/test/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/sensors/integration_test/sensors_test.dart b/packages/sensors/integration_test/sensors_test.dart index ea1db0375f38..348bda00d86e 100644 --- a/packages/sensors/integration_test/sensors_test.dart +++ b/packages/sensors/integration_test/sensors_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 'package:flutter_test/flutter_test.dart'; import 'package:sensors/sensors.dart'; diff --git a/packages/sensors/lib/sensors.dart b/packages/sensors/lib/sensors.dart index 0b6f1b5a6067..8f69255bcec7 100644 --- a/packages/sensors/lib/sensors.dart +++ b/packages/sensors/lib/sensors.dart @@ -121,38 +121,50 @@ GyroscopeEvent _listToGyroscopeEvent(List list) { return GyroscopeEvent(list[0], list[1], list[2]); } -Stream _accelerometerEvents; -Stream _gyroscopeEvents; -Stream _userAccelerometerEvents; +Stream? _accelerometerEvents; +Stream? _gyroscopeEvents; +Stream? _userAccelerometerEvents; /// A broadcast stream of events from the device accelerometer. Stream get accelerometerEvents { - if (_accelerometerEvents == null) { - _accelerometerEvents = _accelerometerEventChannel - .receiveBroadcastStream() - .map( - (dynamic event) => _listToAccelerometerEvent(event.cast())); + Stream? accelerometerEvents = _accelerometerEvents; + if (accelerometerEvents == null) { + accelerometerEvents = + _accelerometerEventChannel.receiveBroadcastStream().map( + (dynamic event) => + _listToAccelerometerEvent(event.cast()), + ); + _accelerometerEvents = accelerometerEvents; } - return _accelerometerEvents; + + return accelerometerEvents; } /// A broadcast stream of events from the device gyroscope. Stream get gyroscopeEvents { - if (_gyroscopeEvents == null) { - _gyroscopeEvents = _gyroscopeEventChannel - .receiveBroadcastStream() - .map((dynamic event) => _listToGyroscopeEvent(event.cast())); + Stream? gyroscopeEvents = _gyroscopeEvents; + if (gyroscopeEvents == null) { + gyroscopeEvents = _gyroscopeEventChannel.receiveBroadcastStream().map( + (dynamic event) => _listToGyroscopeEvent(event.cast()), + ); + _gyroscopeEvents = gyroscopeEvents; } - return _gyroscopeEvents; + + return gyroscopeEvents; } /// Events from the device accelerometer with gravity removed. Stream get userAccelerometerEvents { - if (_userAccelerometerEvents == null) { - _userAccelerometerEvents = _userAccelerometerEventChannel - .receiveBroadcastStream() - .map((dynamic event) => - _listToUserAccelerometerEvent(event.cast())); + Stream? userAccelerometerEvents = + _userAccelerometerEvents; + if (userAccelerometerEvents == null) { + userAccelerometerEvents = + _userAccelerometerEventChannel.receiveBroadcastStream().map( + (dynamic event) => + _listToUserAccelerometerEvent(event.cast()), + ); + _userAccelerometerEvents = userAccelerometerEvents; } - return _userAccelerometerEvents; + + return userAccelerometerEvents; } diff --git a/packages/sensors/pubspec.yaml b/packages/sensors/pubspec.yaml index 4e830d9d0480..da45c82b7dd7 100644 --- a/packages/sensors/pubspec.yaml +++ b/packages/sensors/pubspec.yaml @@ -2,10 +2,7 @@ name: sensors description: Flutter plugin for accessing the Android and iOS accelerometer and gyroscope sensors. 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: 2.0.0 flutter: plugin: @@ -21,14 +18,14 @@ dependencies: sdk: flutter dev_dependencies: - test: ^1.3.0 + test: ^1.16.0 flutter_test: sdk: flutter integration_test: path: ../integration_test - mockito: ^4.1.1 - pedantic: ^1.8.0 + mockito: ^5.0.0-nullsafety.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0<3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" diff --git a/packages/sensors/test/sensors_test.dart b/packages/sensors/test/sensors_test.dart index 832a2f8524b7..93e0959befed 100644 --- a/packages/sensors/test/sensors_test.dart +++ b/packages/sensors/test/sensors_test.dart @@ -52,16 +52,16 @@ void main() { void _initializeFakeSensorChannel(String channelName, List sensorData) { const StandardMethodCodec standardMethod = StandardMethodCodec(); - void _emitEvent(ByteData event) { - ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + void _emitEvent(ByteData? event) { + ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( channelName, event, - (ByteData reply) {}, + (ByteData? reply) {}, ); } - ServicesBinding.instance.defaultBinaryMessenger - .setMockMessageHandler(channelName, (ByteData message) async { + ServicesBinding.instance!.defaultBinaryMessenger + .setMockMessageHandler(channelName, (ByteData? message) async { final MethodCall methodCall = standardMethod.decodeMethodCall(message); if (methodCall.method == 'listen') { _emitEvent(standardMethod.encodeSuccessEnvelope(sensorData)); diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md index 883cd340ecfb..20afdea9f054 100644 --- a/packages/share/CHANGELOG.md +++ b/packages/share/CHANGELOG.md @@ -1,3 +1,14 @@ +## 2.0.0 + +* Migrate to null safety. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Update README with the new documentation urls. + +## 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..59cfbf7c5665 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. @@ -8,16 +8,9 @@ share dialog. Wraps the ACTION_SEND Intent on Android and UIActivityViewController on iOS. -**Please set your constraint to `share: '>=0.6.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.6.y+z`. -Please use `share: '>=0.6.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## 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/packages-and-plugins/using-packages/). ## Example @@ -44,4 +37,4 @@ To share one or multiple files invoke the static `shareFiles` method anywhere in ``` dart Share.shareFiles(['${directory.path}/image.jpg'], text: 'Great picture'); Share.shareFiles(['${directory.path}/image1.jpg', '${directory.path}/image2.jpg']); -``` \ No newline at end of file +``` 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/image_previews.dart b/packages/share/example/lib/image_previews.dart index 61ecec43bdc7..9070749267fc 100644 --- a/packages/share/example/lib/image_previews.dart +++ b/packages/share/example/lib/image_previews.dart @@ -9,11 +9,11 @@ class ImagePreviews extends StatelessWidget { final List imagePaths; /// Callback when an image should be removed - final Function(int) onDelete; + final Function(int)? onDelete; /// Creates a widget for preview of images. [imagePaths] can not be empty /// and all contained paths need to be non empty. - const ImagePreviews(this.imagePaths, {Key key, this.onDelete}) + const ImagePreviews(this.imagePaths, {Key? key, this.onDelete}) : super(key: key); @override @@ -26,7 +26,7 @@ class ImagePreviews extends StatelessWidget { for (int i = 0; i < imagePaths.length; i++) { imageWidgets.add(_ImagePreview( imagePaths[i], - onDelete: onDelete != null ? () => onDelete(i) : null, + onDelete: onDelete != null ? () => onDelete!(i) : null, )); } @@ -39,9 +39,9 @@ class ImagePreviews extends StatelessWidget { class _ImagePreview extends StatelessWidget { final String imagePath; - final VoidCallback onDelete; + final VoidCallback? onDelete; - const _ImagePreview(this.imagePath, {Key key, this.onDelete}) + const _ImagePreview(this.imagePath, {Key? key, this.onDelete}) : super(key: key); @override diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index 7ebce3ce5591..8d6a78305db9 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,13 +110,13 @@ 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(); + final RenderBox box = context.findRenderObject() as RenderBox; if (imagePaths.isNotEmpty) { await Share.shareFiles(imagePaths, diff --git a/packages/share/example/pubspec.yaml b/packages/share/example/pubspec.yaml index 8b8623910b7a..2df76efb6ca2 100644 --- a/packages/share/example/pubspec.yaml +++ b/packages/share/example/pubspec.yaml @@ -5,19 +5,24 @@ dependencies: flutter: sdk: flutter share: + # When depending on this package from a real application you should use: + # share: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - image_picker: ^0.6.7+4 + image_picker: ^0.7.0 dev_dependencies: flutter_driver: sdk: flutter integration_test: path: ../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.9.1+hotfix.2" diff --git a/packages/share/example/test_driver/test/integration_test.dart b/packages/share/example/test_driver/test/integration_test.dart index 7a2c21338786..a8a56aa90f6a 100644 --- a/packages/share/example/test_driver/test/integration_test.dart +++ b/packages/share/example/test_driver/test/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/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..e8a116799433 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 flutter: plugin: @@ -17,20 +14,20 @@ flutter: pluginClass: FLTSharePlugin dependencies: - meta: ^1.0.5 - mime: ^0.9.7 + meta: ^1.3.0 + mime: ^1.0.0 flutter: sdk: flutter dev_dependencies: - test: ^1.3.0 - mockito: ^3.0.0 + test: ^1.16.3 + mockito: ^5.0.0-nullsafety.7 flutter_test: sdk: flutter integration_test: path: ../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 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-259.9.beta <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..1516163b8807 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,35 @@ +## 2.0.3 + +* Android: don't create additional Handler when method channel is called. + +## 2.0.2 + +* Don't create additional thread pools when method channel is called. + +## 2.0.1 + +* Removed deprecated [AsyncTask](https://developer.android.com/reference/android/os/AsyncTask) was deprecated in API level 30 ([#3481](https://github.com/flutter/plugins/pull/3481)) + +## 2.0.0 + +* Migrate to null-safety. + +**Breaking changes**: + +* Setters no longer accept null to mean removing values. If you were previously using `set*(key, null)` for removing, use `remove(key)` instead. + +## 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..e51ddea1c890 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -1,22 +1,14 @@ # 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, and there is no guarantee that writes will be persisted to disk after returning, so this plugin must not be used for storing critical data. - -**Please set your constraint to `shared_preferences: '>=0.5.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.5.y+z`. -Please use `shared_preferences: '>=0.5.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -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/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties index caf54fa2801c..3c9d0852bfa5 100644 --- a/packages/shared_preferences/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/shared_preferences/shared_preferences/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-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java index 33f2474592fa..4f55d882005f 100644 --- a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java +++ b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java @@ -6,7 +6,8 @@ import android.content.Context; import android.content.SharedPreferences; -import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; import android.util.Base64; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -21,6 +22,10 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; /** * Implementation of the {@link MethodChannel.MethodCallHandler} for the plugin. It is also @@ -38,12 +43,18 @@ class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private final android.content.SharedPreferences preferences; + private final ExecutorService executor; + private final Handler handler; + /** * Constructs a {@link MethodCallHandlerImpl} instance. Creates a {@link * android.content.SharedPreferences} based on the {@code context}. */ MethodCallHandlerImpl(Context context) { preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + executor = + new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new SynchronousQueue()); + handler = new Handler(Looper.getMainLooper()); } @Override @@ -116,19 +127,27 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { } } + public void teardown() { + handler.removeCallbacksAndMessages(null); + executor.shutdown(); + } + private void commitAsync( final SharedPreferences.Editor editor, final MethodChannel.Result result) { - new AsyncTask() { - @Override - protected Boolean doInBackground(Void... voids) { - return editor.commit(); - } - - @Override - protected void onPostExecute(Boolean value) { - result.success(value); - } - }.execute(); + executor.execute( + new Runnable() { + @Override + public void run() { + final boolean response = editor.commit(); + handler.post( + new Runnable() { + @Override + public void run() { + result.success(response); + } + }); + } + }); } private List decodeList(String encodedList) throws IOException { diff --git a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java index be627f3ce613..83163f82d9ec 100644 --- a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java +++ b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java @@ -13,6 +13,7 @@ public class SharedPreferencesPlugin implements FlutterPlugin { private static final String CHANNEL_NAME = "plugins.flutter.io/shared_preferences"; private MethodChannel channel; + private MethodCallHandlerImpl handler; @SuppressWarnings("deprecation") public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { @@ -32,11 +33,13 @@ public void onDetachedFromEngine(FlutterPlugin.FlutterPluginBinding binding) { private void setupChannel(BinaryMessenger messenger, Context context) { channel = new MethodChannel(messenger, CHANNEL_NAME); - MethodCallHandlerImpl handler = new MethodCallHandlerImpl(context); + handler = new MethodCallHandlerImpl(context); channel.setMethodCallHandler(handler); } private void teardownChannel() { + handler.teardown(); + handler = null; channel.setMethodCallHandler(null); channel = null; } diff --git a/packages/shared_preferences/shared_preferences/example/.gitignore b/packages/shared_preferences/shared_preferences/example/.gitignore new file mode 100644 index 000000000000..0fa6b675c0a5 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/shared_preferences/shared_preferences/example/.metadata b/packages/shared_preferences/shared_preferences/example/.metadata new file mode 100644 index 000000000000..e0e9530fccc9 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 79b49b9e1057f90ebf797725233c6b311722de69 + channel: dev + +project_type: app diff --git a/packages/shared_preferences/shared_preferences/example/android/.gitignore b/packages/shared_preferences/shared_preferences/example/android/.gitignore new file mode 100644 index 000000000000..0a741cb43d66 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/packages/shared_preferences/shared_preferences/example/android/app/build.gradle b/packages/shared_preferences/shared_preferences/example/android/app/build.gradle index 58a6567a161c..3b7ee369beee 100644 --- a/packages/shared_preferences/shared_preferences/example/android/app/build.gradle +++ b/packages/shared_preferences/shared_preferences/example/android/app/build.gradle @@ -22,22 +22,23 @@ if (flutterVersionName == null) { } apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 30 - lintOptions { - disable 'InvalidPackage' + sourceSets { + main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - applicationId "io.flutter.plugins.sharedpreferencesexample" + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "io.flutter.plugins.example" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -54,7 +55,5 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/packages/shared_preferences/shared_preferences/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences/example/android/app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ee69dd68d1a6..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/debug/AndroidManifest.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..2d5b32857609 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml index 04a642a4c748..2a12ff8e0009 100644 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml @@ -1,26 +1,41 @@ - - - - - - - + + + + + + - + + diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1Activity.java b/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1Activity.java deleted file mode 100644 index 3857aea6ce6b..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,22 +0,0 @@ -// 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. - -package io.flutter.plugins.sharedpreferencesexample; - -import android.os.Bundle; -import dev.flutter.plugins.integration_test.IntegrationTestPlugin; -import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - IntegrationTestPlugin.registerWith( - registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); - SharedPreferencesPlugin.registerWith( - registrarFor("io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")); - } -} diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java b/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index 5b090d0f52c2..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,15 +0,0 @@ - -package io.flutter.plugins.sharedpreferencesexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/FlutterActivityTest.java b/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/FlutterActivityTest.java deleted file mode 100644 index 7a63d6d90c91..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/FlutterActivityTest.java +++ /dev/null @@ -1,17 +0,0 @@ -// 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.sharedpreferencesexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/kotlin/io/flutter/plugins/example/MainActivity.kt b/packages/shared_preferences/shared_preferences/example/android/app/src/main/kotlin/io/flutter/plugins/example/MainActivity.kt new file mode 100644 index 000000000000..9059dae9e4c4 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/kotlin/io/flutter/plugins/example/MainActivity.kt @@ -0,0 +1,6 @@ +package io.flutter.plugins.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable/launch_background.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/values-night/styles.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..449a9f930826 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/values/styles.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..d74aa35c2826 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/profile/AndroidManifest.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..2d5b32857609 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/shared_preferences/shared_preferences/example/android/build.gradle b/packages/shared_preferences/shared_preferences/example/android/build.gradle index 54cc96612793..c505a8635265 100644 --- a/packages/shared_preferences/shared_preferences/example/android/build.gradle +++ b/packages/shared_preferences/shared_preferences/example/android/build.gradle @@ -1,11 +1,13 @@ buildscript { + ext.kotlin_version = '1.3.50' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/packages/shared_preferences/shared_preferences/example/android/gradle.properties b/packages/shared_preferences/shared_preferences/example/android/gradle.properties index a6738207fd15..94adc3a3f97a 100644 --- a/packages/shared_preferences/shared_preferences/example/android/gradle.properties +++ b/packages/shared_preferences/shared_preferences/example/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -android.enableR8=true diff --git a/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties index d757f3d33fcc..bc6a58afdda2 100644 --- a/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/packages/shared_preferences/shared_preferences/example/android/settings.gradle b/packages/shared_preferences/shared_preferences/example/android/settings.gradle index 115da6cb4f4d..44e62bcf06ae 100644 --- a/packages/shared_preferences/shared_preferences/example/android/settings.gradle +++ b/packages/shared_preferences/shared_preferences/example/android/settings.gradle @@ -1,15 +1,11 @@ include ':app' -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withInputStream { stream -> plugins.load(stream) } -} +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 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..1caf695d9365 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 @@ -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 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -33,7 +39,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 +52,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 +67,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 +78,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/espresso/example/ios/.gitignore b/packages/shared_preferences/shared_preferences/example/ios/.gitignore similarity index 100% rename from packages/espresso/example/ios/.gitignore rename to packages/shared_preferences/shared_preferences/example/ios/.gitignore diff --git a/packages/shared_preferences/shared_preferences/example/ios/Flutter/Debug.xcconfig b/packages/shared_preferences/shared_preferences/example/ios/Flutter/Debug.xcconfig index 9803018ca79d..d0eccdcaf401 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Flutter/Debug.xcconfig +++ b/packages/shared_preferences/shared_preferences/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/shared_preferences/shared_preferences/example/ios/Flutter/Release.xcconfig b/packages/shared_preferences/shared_preferences/example/ios/Flutter/Release.xcconfig index a4a8c604e13d..c751c1d022fa 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Flutter/Release.xcconfig +++ b/packages/shared_preferences/shared_preferences/example/ios/Flutter/Release.xcconfig @@ -1,2 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/android_intent/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 71% rename from packages/android_intent/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..919434a6254f 100644 --- a/packages/android_intent/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Runner.entitlements b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 78% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Runner.entitlements rename to packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index ba21fbdaf290..18d981003d68 100644 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/Runner.entitlements +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -2,7 +2,7 @@ - com.apple.developer.networking.wifi-info + IDEDidComputeMac32BitWarning diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/android_alarm_manager/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/shared_preferences/shared_preferences/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/espresso/example/ios/Runner/AppDelegate.swift b/packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.swift similarity index 100% rename from packages/espresso/example/ios/Runner/AppDelegate.swift rename to packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.swift diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner/Runner-Bridging-Header.h b/packages/shared_preferences/shared_preferences/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000000..308a2a560b42 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/shared_preferences/shared_preferences/example/lib/main.dart b/packages/shared_preferences/shared_preferences/example/lib/main.dart index 46daeff6706f..26e6c8eb42f8 100644 --- a/packages/shared_preferences/shared_preferences/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences/example/lib/main.dart @@ -24,7 +24,7 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key key}) : super(key: key); + SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); @@ -32,7 +32,7 @@ class SharedPreferencesDemo extends StatefulWidget { class SharedPreferencesDemoState extends State { Future _prefs = SharedPreferences.getInstance(); - Future _counter; + late Future _counter; Future _incrementCounter() async { final SharedPreferences prefs = await _prefs; diff --git a/packages/shared_preferences/shared_preferences/example/macos/.gitignore b/packages/shared_preferences/shared_preferences/example/macos/.gitignore new file mode 100644 index 000000000000..d2fd3772308c --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/macos/.gitignore @@ -0,0 +1,6 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/xcuserdata/ diff --git a/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Debug.xcconfig index 785633d3a86b..4b81f9b2d200 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Debug.xcconfig +++ b/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Debug.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Release.xcconfig b/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Release.xcconfig index 5fba960c3af2..5caa9d1579e4 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Release.xcconfig +++ b/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Release.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.pbxproj index 0e2413493f6e..cc89c8782812 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.pbxproj @@ -26,11 +26,6 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - EA473EC5F2038B17A2FE4D78 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 748ADDF1719804343BB18004 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -50,8 +45,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -61,7 +54,7 @@ /* Begin PBXFileReference section */ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* connectivity_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = connectivity_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -70,17 +63,11 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 748ADDF1719804343BB18004 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 80418F0A2F74D683C63A4D0A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - AA19B00394637215A825CF5E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; - E960ED3977AF6DF197F74FFA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,9 +75,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, - EA473EC5F2038B17A2FE4D78 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -115,14 +99,13 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - D42EAEE5849744148CC78D83 /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* connectivity_example.app */, + 33CC10ED2044A3C60003C045 /* example.app */, ); name = Products; sourceTree = ""; @@ -145,8 +128,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -164,21 +145,9 @@ path = Runner; sourceTree = ""; }; - D42EAEE5849744148CC78D83 /* Pods */ = { - isa = PBXGroup; - children = ( - 80418F0A2F74D683C63A4D0A /* Pods-Runner.debug.xcconfig */, - E960ED3977AF6DF197F74FFA /* Pods-Runner.release.xcconfig */, - AA19B00394637215A825CF5E /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - 748ADDF1719804343BB18004 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -190,13 +159,11 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - B24477CAB9D5BDFC8F3553DA /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 84A8D21305B2F01D093A8F9C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -205,7 +172,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* connectivity_example.app */; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -216,7 +183,7 @@ attributes = { LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 0930; - ORGANIZATIONNAME = "Google LLC"; + ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; @@ -235,7 +202,7 @@ }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 8.0"; + compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -281,7 +248,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -301,44 +268,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh\ntouch Flutter/ephemeral/tripwire\n"; - }; - 84A8D21305B2F01D093A8F9C /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - B24477CAB9D5BDFC8F3553DA /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; /* End PBXShellScriptBuildPhase section */ @@ -431,10 +361,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -561,10 +487,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -585,10 +507,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 2a7d3e7f34ac..ae8ff59d97b3 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -27,23 +27,11 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - @@ -66,7 +54,7 @@ @@ -75,7 +63,7 @@ diff --git a/packages/shared_preferences/shared_preferences/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/shared_preferences/shared_preferences/example/macos/Runner/Configs/AppInfo.xcconfig index a95148814518..cccda2e8a262 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/shared_preferences/shared_preferences/example/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = connectivity_example +PRODUCT_NAME = example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.connectivityExample +PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.example // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 io.flutter.plugins. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2021 io.flutter.plugins. All rights reserved. diff --git a/packages/shared_preferences/shared_preferences/example/pubspec.yaml b/packages/shared_preferences/shared_preferences/example/pubspec.yaml index cf0fa64fea46..84692d76e5a1 100644 --- a/packages/shared_preferences/shared_preferences/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/example/pubspec.yaml @@ -5,6 +5,11 @@ dependencies: flutter: sdk: flutter shared_preferences: + # When depending on this package from a real application you should use: + # shared_preferences: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -12,12 +17,11 @@ dev_dependencies: sdk: flutter integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2 <2.0.0" - + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.9.1+hotfix.2" diff --git a/packages/shared_preferences/shared_preferences/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences/example/test_driver/integration_test.dart index 7a2c21338786..ac106b63b339 100644 --- a/packages/shared_preferences/shared_preferences/example/test_driver/integration_test.dart +++ b/packages/shared_preferences/shared_preferences/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/shared_preferences/shared_preferences/example/web/favicon.png b/packages/shared_preferences/shared_preferences/example/web/favicon.png new file mode 100644 index 000000000000..8aaa46ac1ae2 Binary files /dev/null and b/packages/shared_preferences/shared_preferences/example/web/favicon.png differ diff --git a/packages/shared_preferences/shared_preferences/example/web/icons/Icon-192.png b/packages/shared_preferences/shared_preferences/example/web/icons/Icon-192.png new file mode 100644 index 000000000000..b749bfef0747 Binary files /dev/null and b/packages/shared_preferences/shared_preferences/example/web/icons/Icon-192.png differ diff --git a/packages/shared_preferences/shared_preferences/example/web/icons/Icon-512.png b/packages/shared_preferences/shared_preferences/example/web/icons/Icon-512.png new file mode 100644 index 000000000000..88cfd48dff11 Binary files /dev/null and b/packages/shared_preferences/shared_preferences/example/web/icons/Icon-512.png differ diff --git a/packages/shared_preferences/shared_preferences/example/web/manifest.json b/packages/shared_preferences/shared_preferences/example/web/manifest.json new file mode 100644 index 000000000000..8c012917dab7 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index c4c8710769df..2f4ebe730351 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -7,10 +7,9 @@ import 'dart:io' show Platform; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:meta/meta.dart'; - import 'package:shared_preferences_linux/shared_preferences_linux.dart'; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:shared_preferences_windows/shared_preferences_windows.dart'; /// Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing @@ -21,7 +20,7 @@ class SharedPreferences { SharedPreferences._(this._preferenceCache); static const String _prefix = 'flutter.'; - static Completer _completer; + static Completer? _completer; static bool _manualDartRegistrationNeeded = true; static SharedPreferencesStorePlatform get _store { @@ -52,21 +51,22 @@ class SharedPreferences { /// performance-sensitive blocks. static Future getInstance() async { if (_completer == null) { - _completer = Completer(); + final completer = Completer(); try { final Map preferencesMap = await _getSharedPreferencesMap(); - _completer.complete(SharedPreferences._(preferencesMap)); + completer.complete(SharedPreferences._(preferencesMap)); } on Exception catch (e) { // If there's an error, explicitly return the future with an error. // then set the completer to null so we can retry. - _completer.completeError(e); - final Future sharedPrefsFuture = _completer.future; + completer.completeError(e); + final Future sharedPrefsFuture = completer.future; _completer = null; return sharedPrefsFuture; } + _completer = completer; } - return _completer.future; + return _completer!.future; } /// The cache that holds all preferences. @@ -83,86 +83,76 @@ class SharedPreferences { Set getKeys() => Set.from(_preferenceCache.keys); /// Reads a value of any type from persistent storage. - dynamic get(String key) => _preferenceCache[key]; + Object? get(String key) => _preferenceCache[key]; /// Reads a value from persistent storage, throwing an exception if it's not a /// bool. - bool getBool(String key) => _preferenceCache[key]; + bool? getBool(String key) => _preferenceCache[key] as bool?; /// Reads a value from persistent storage, throwing an exception if it's not /// an int. - int getInt(String key) => _preferenceCache[key]; + int? getInt(String key) => _preferenceCache[key] as int?; /// Reads a value from persistent storage, throwing an exception if it's not a /// double. - double getDouble(String key) => _preferenceCache[key]; + double? getDouble(String key) => _preferenceCache[key] as double?; /// Reads a value from persistent storage, throwing an exception if it's not a /// String. - String getString(String key) => _preferenceCache[key]; + String? getString(String key) => _preferenceCache[key] as String?; /// Returns true if persistent storage the contains the given [key]. bool containsKey(String key) => _preferenceCache.containsKey(key); /// Reads a set of string values from persistent storage, throwing an /// exception if it's not a string set. - List getStringList(String key) { - List list = _preferenceCache[key]; + List? getStringList(String key) { + List? list = _preferenceCache[key] as List?; if (list != null && list is! List) { list = list.cast().toList(); _preferenceCache[key] = list; } // Make a copy of the list so that later mutations won't propagate - return list?.toList(); + return list?.toList() as List?; } /// Saves a boolean [value] to persistent storage in the background. - /// - /// If [value] is null, this is equivalent to calling [remove()] on the [key]. Future setBool(String key, bool value) => _setValue('Bool', key, value); /// Saves an integer [value] to persistent storage in the background. - /// - /// If [value] is null, this is equivalent to calling [remove()] on the [key]. Future setInt(String key, int value) => _setValue('Int', key, value); /// Saves a double [value] to persistent storage in the background. /// /// Android doesn't support storing doubles, so it will be stored as a float. - /// - /// If [value] is null, this is equivalent to calling [remove()] on the [key]. Future setDouble(String key, double value) => _setValue('Double', key, value); /// Saves a string [value] to persistent storage in the background. - /// - /// If [value] is null, this is equivalent to calling [remove()] on the [key]. Future setString(String key, String value) => _setValue('String', key, value); /// Saves a list of strings [value] to persistent storage in the background. - /// - /// If [value] is null, this is equivalent to calling [remove()] on the [key]. Future setStringList(String key, List value) => _setValue('StringList', key, value); /// Removes an entry from persistent storage. - Future remove(String key) => _setValue(null, key, null); + Future remove(String key) { + final String prefixedKey = '$_prefix$key'; + _preferenceCache.remove(key); + return _store.remove(prefixedKey); + } Future _setValue(String valueType, String key, Object value) { + ArgumentError.checkNotNull(value, 'value'); final String prefixedKey = '$_prefix$key'; - if (value == null) { - _preferenceCache.remove(key); - return _store.remove(prefixedKey); + if (value is List) { + // Make a copy of the list so that later mutations won't propagate + _preferenceCache[key] = value.toList(); } else { - if (value is List) { - // Make a copy of the list so that later mutations won't propagate - _preferenceCache[key] = value.toList(); - } else { - _preferenceCache[key] = value; - } - return _store.setValue(valueType, prefixedKey, value); + _preferenceCache[key] = value; } + return _store.setValue(valueType, prefixedKey, value); } /// Always returns true. @@ -194,7 +184,7 @@ class SharedPreferences { final Map preferencesMap = {}; for (String key in fromSystem.keys) { assert(key.startsWith(_prefix)); - preferencesMap[key.substring(_prefix.length)] = fromSystem[key]; + preferencesMap[key.substring(_prefix.length)] = fromSystem[key]!; } return preferencesMap; } @@ -203,14 +193,14 @@ class SharedPreferences { /// /// If the singleton instance has been initialized already, it is nullified. @visibleForTesting - static void setMockInitialValues(Map values) { - final Map newValues = - values.map((String key, dynamic value) { + static void setMockInitialValues(Map values) { + final Map newValues = + values.map((String key, Object value) { String newKey = key; if (!key.startsWith(_prefix)) { newKey = '$_prefix$key'; } - return MapEntry(newKey, value); + return MapEntry(newKey, value); }); SharedPreferencesStorePlatform.instance = InMemorySharedPreferencesStore.withData(newValues); diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index c09e2c64d7f9..899266a4d6f0 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -2,10 +2,7 @@ name: shared_preferences description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences -# 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: 2.0.3 flutter: plugin: @@ -23,19 +20,19 @@ flutter: default_package: shared_preferences_web dependencies: - meta: ^1.0.4 + meta: ^1.3.0 flutter: sdk: flutter - shared_preferences_platform_interface: ^1.0.0 + shared_preferences_platform_interface: ^2.0.0 # 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(franciscojma): Revisit this (either update this part in the design or the pub tool). # https://github.com/flutter/flutter/issues/46264 - shared_preferences_linux: ^0.0.2 - shared_preferences_macos: ^0.0.1 - shared_preferences_web: ^0.1.2 - shared_preferences_windows: ^0.0.1 + shared_preferences_linux: ^2.0.0 + shared_preferences_macos: ^2.0.0 + shared_preferences_web: ^2.0.0 + shared_preferences_windows: ^2.0.0 dev_dependencies: flutter_test: @@ -44,8 +41,8 @@ dev_dependencies: sdk: flutter integration_test: path: ../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.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..7866b2e38fac 100755 --- a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_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/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -13,7 +11,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferences', () { - const Map kTestValues = { + const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, 'flutter.int': 42, @@ -29,8 +27,8 @@ void main() { 'flutter.List': ['baz', 'quox'], }; - FakeSharedPreferencesStore store; - SharedPreferences preferences; + late FakeSharedPreferencesStore store; + late SharedPreferences preferences; setUp(() async { store = FakeSharedPreferencesStore(kTestValues); @@ -41,6 +39,7 @@ void main() { tearDown(() async { await preferences.clear(); + await store.clear(); }); test('reading', () async { @@ -107,16 +106,11 @@ void main() { test('removing', () async { const String key = 'testKey'; - await preferences.setString(key, null); - await preferences.setBool(key, null); - await preferences.setInt(key, null); - await preferences.setDouble(key, null); - await preferences.setStringList(key, null); await preferences.remove(key); expect( store.log, List.filled( - 6, + 1, isMethodCall( 'remove', arguments: 'flutter.$key', @@ -145,10 +139,12 @@ void main() { }); test('reloading', () async { - await preferences.setString('String', kTestValues['flutter.String']); + await preferences.setString( + 'String', kTestValues['flutter.String'] as String); expect(preferences.getString('String'), kTestValues['flutter.String']); - SharedPreferences.setMockInitialValues(kTestValues2); + SharedPreferences.setMockInitialValues( + kTestValues2.cast()); expect(preferences.getString('String'), kTestValues['flutter.String']); await preferences.reload(); @@ -161,23 +157,32 @@ void main() { expect(await first, await second); }); + test('string list type is dynamic (usually from method channel)', () async { + SharedPreferences.setMockInitialValues({ + 'dynamic_list': ['1', '2'] + }); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final List? value = prefs.getStringList('dynamic_list'); + expect(value, ['1', '2']); + }); + group('mocking', () { const String _key = 'dummy'; const String _prefixedKey = 'flutter.' + _key; test('test 1', () async { SharedPreferences.setMockInitialValues( - {_prefixedKey: 'my string'}); + {_prefixedKey: 'my string'}); final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String value = prefs.getString(_key); + final String? value = prefs.getString(_key); expect(value, 'my string'); }); test('test 2', () async { SharedPreferences.setMockInitialValues( - {_prefixedKey: 'my other string'}); + {_prefixedKey: 'my other string'}); final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String value = prefs.getString(_key); + final String? value = prefs.getString(_key); expect(value, 'my other string'); }); }); @@ -187,7 +192,7 @@ void main() { await preferences.setStringList("myList", myList); myList.add("foobar"); - final List cachedList = preferences.getStringList('myList'); + final List cachedList = preferences.getStringList('myList')!; expect(cachedList, []); cachedList.add("foobar2"); @@ -197,11 +202,11 @@ void main() { }); test('calling mock initial values with non-prefixed keys succeeds', () async { - SharedPreferences.setMockInitialValues({ + SharedPreferences.setMockInitialValues({ 'test': 'foo', }); final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String value = prefs.getString('test'); + final String? value = prefs.getString('test'); expect(value, 'foo'); }); } diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index dd67393b07b0..a50b4e470c53 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,3 +1,15 @@ +## 2.0.0 + +* Migrate to null-safety. + +## 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..019dc248a918 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 @@ -1,12 +1,19 @@ +// 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 'dart:async'; + import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences_linux/shared_preferences_linux.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('$SharedPreferences', () { + group('SharedPreferencesLinux', () { const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, @@ -23,67 +30,73 @@ void main() { 'flutter.List': ['baz', 'quox'], }; - SharedPreferences preferences; + SharedPreferencesLinux preferences; setUp(() async { - preferences = await SharedPreferences.getInstance(); + preferences = SharedPreferencesLinux.instance; }); tearDown(() { preferences.clear(); }); - test('reading', () async { - expect(preferences.get('String'), isNull); - expect(preferences.get('bool'), isNull); - expect(preferences.get('int'), isNull); - expect(preferences.get('double'), isNull); - expect(preferences.get('List'), isNull); - expect(preferences.getString('String'), isNull); - expect(preferences.getBool('bool'), isNull); - expect(preferences.getInt('int'), isNull); - expect(preferences.getDouble('double'), isNull); - expect(preferences.getStringList('List'), isNull); + testWidgets('reading', (WidgetTester _) async { + final all = await preferences.getAll(); + expect(all['String'], isNull); + expect(all['bool'], isNull); + expect(all['int'], isNull); + expect(all['double'], isNull); + expect(all['List'], isNull); }); - test('writing', () async { + testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ - preferences.setString('String', kTestValues2['flutter.String']), - preferences.setBool('bool', kTestValues2['flutter.bool']), - preferences.setInt('int', kTestValues2['flutter.int']), - preferences.setDouble('double', kTestValues2['flutter.double']), - preferences.setStringList('List', kTestValues2['flutter.List']) + preferences.setValue( + 'String', 'String', kTestValues2['flutter.String']), + preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']), + preferences.setValue('Int', 'int', kTestValues2['flutter.int']), + preferences.setValue( + 'Double', 'double', kTestValues2['flutter.double']), + preferences.setValue('StringList', 'List', kTestValues2['flutter.List']) ]); - expect(preferences.getString('String'), kTestValues2['flutter.String']); - expect(preferences.getBool('bool'), kTestValues2['flutter.bool']); - expect(preferences.getInt('int'), kTestValues2['flutter.int']); - expect(preferences.getDouble('double'), kTestValues2['flutter.double']); - expect(preferences.getStringList('List'), kTestValues2['flutter.List']); + final all = await preferences.getAll(); + expect(all['String'], kTestValues2['flutter.String']); + expect(all['bool'], kTestValues2['flutter.bool']); + expect(all['int'], kTestValues2['flutter.int']); + expect(all['double'], kTestValues2['flutter.double']); + expect(all['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']); - await preferences.setInt(key, kTestValues['flutter.int']); - await preferences.setDouble(key, kTestValues['flutter.double']); - await preferences.setStringList(key, kTestValues['flutter.List']); + + await Future.wait([ + preferences.setValue('String', key, kTestValues['flutter.String']), + preferences.setValue('Bool', key, kTestValues['flutter.bool']), + preferences.setValue('Int', key, kTestValues['flutter.int']), + preferences.setValue('Double', key, kTestValues['flutter.double']), + preferences.setValue('StringList', key, kTestValues['flutter.List']) + ]); await preferences.remove(key); - expect(preferences.get('testKey'), isNull); + final all = await preferences.getAll(); + expect(all['testKey'], isNull); }); - test('clearing', () async { - await preferences.setString('String', kTestValues['flutter.String']); - await preferences.setBool('bool', kTestValues['flutter.bool']); - await preferences.setInt('int', kTestValues['flutter.int']); - await preferences.setDouble('double', kTestValues['flutter.double']); - await preferences.setStringList('List', kTestValues['flutter.List']); + testWidgets('clearing', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue('String', 'String', kTestValues['flutter.String']), + preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']), + preferences.setValue('Int', 'int', kTestValues['flutter.int']), + preferences.setValue('Double', 'double', kTestValues['flutter.double']), + preferences.setValue('StringList', 'List', kTestValues['flutter.List']) + ]); await preferences.clear(); - expect(preferences.getString('String'), null); - expect(preferences.getBool('bool'), null); - expect(preferences.getInt('int'), null); - expect(preferences.getDouble('double'), null); - expect(preferences.getStringList('List'), null); + final all = await preferences.getAll(); + expect(all['String'], null); + expect(all['bool'], null); + expect(all['int'], null); + expect(all['double'], null); + expect(all['List'], null); }); }); } diff --git a/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart b/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart index ceacf2f95f28..ab664cd652ff 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart @@ -24,7 +24,7 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key key}) : super(key: key); + SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); @@ -32,14 +32,14 @@ class SharedPreferencesDemo extends StatefulWidget { class SharedPreferencesDemoState extends State { final prefs = SharedPreferencesLinux.instance; - Future _counter; + late Future _counter; Future _incrementCounter() async { final values = await prefs.getAll(); - final int counter = (values['counter'] as int ?? 0) + 1; + final int counter = (values['counter'] as int? ?? 0) + 1; setState(() { - _counter = prefs.setValue(null, "counter", counter).then((bool success) { + _counter = prefs.setValue('Int', 'counter', counter).then((bool success) { return counter; }); }); @@ -49,7 +49,7 @@ class SharedPreferencesDemoState extends State { void initState() { super.initState(); _counter = prefs.getAll().then((Map values) { - return (values['counter'] ?? 0); + return (values['counter'] as int? ?? 0); }); } diff --git a/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml index fb79444dc6cd..dffdbd7526d2 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml @@ -4,26 +4,24 @@ description: Demonstrates how to use the shared_preferences_linux plugin. dependencies: flutter: sdk: flutter - shared_preferences: any - shared_preferences_linux: ^0.1.0 - -dependency_overrides: shared_preferences_linux: + # When depending on this package from a real application you should use: + # shared_preferences_linux: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - # Remove this override once the endorsement is published. - shared_preferences: - path: ../../shared_preferences/ dev_dependencies: flutter_driver: sdk: flutter integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/shared_preferences/shared_preferences_linux/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_linux/example/test_driver/integration_test.dart index 7a2c21338786..ac106b63b339 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/test_driver/integration_test.dart +++ b/packages/shared_preferences/shared_preferences_linux/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/shared_preferences/shared_preferences_linux/ios/shared_preferences_linux.podspec b/packages/shared_preferences/shared_preferences_linux/ios/shared_preferences_linux.podspec deleted file mode 100644 index 8f4d3cdddcd5..000000000000 --- a/packages/shared_preferences/shared_preferences_linux/ios/shared_preferences_linux.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint shared_preferences_launcher_linux.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'shared_preferences_linux' - s.version = '0.0.1' - s.summary = 'shared_preferences_linux iOS stub' - s.description = <<-DESC - No-op implementation of the Linux shared_preferences plugin to avoid build issues on iOS - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_linux' } - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart index c975ad1a7544..5a694658cdf5 100644 --- a/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart +++ b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart @@ -20,16 +20,17 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { static SharedPreferencesLinux instance = SharedPreferencesLinux(); /// Local copy of preferences - Map _cachedPreferences; + Map? _cachedPreferences; /// File system used to store to disk. Exposed for testing only. @visibleForTesting FileSystem fs = LocalFileSystem(); /// Gets the file where the preferences are stored. - Future _getLocalDataFile() async { + Future _getLocalDataFile() async { final pathProvider = PathProviderLinux(); final directory = await pathProvider.getApplicationSupportPath(); + if (directory == null) return null; return fs.file(path.join(directory, 'shared_preferences.json')); } @@ -37,19 +38,19 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { /// maintained in memory. Future> _readPreferences() async { if (_cachedPreferences != null) { - return _cachedPreferences; + return _cachedPreferences!; } - _cachedPreferences = {}; - var localDataFile = await _getLocalDataFile(); - if (localDataFile.existsSync()) { + Map preferences = {}; + final File? localDataFile = await _getLocalDataFile(); + if (localDataFile != null && localDataFile.existsSync()) { String stringMap = localDataFile.readAsStringSync(); if (stringMap.isNotEmpty) { - _cachedPreferences = json.decode(stringMap) as Map; + preferences = json.decode(stringMap).cast(); } } - - return _cachedPreferences; + _cachedPreferences = preferences; + return preferences; } /// Writes the cached preferences to disk. Returns [true] if the operation @@ -57,6 +58,10 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { Future _writePreferences(Map preferences) async { try { var localDataFile = await _getLocalDataFile(); + if (localDataFile == null) { + print("Unable to determine where to write preferences."); + return false; + } if (!localDataFile.existsSync()) { localDataFile.createSync(recursive: true); } diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index 380cee9efb1c..0730ca8d5fec 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: 2.0.0 homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_linux flutter: @@ -11,19 +11,19 @@ flutter: pluginClass: none environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" dependencies: - file: ">=5.1.0 <7.0.0" flutter: sdk: flutter - meta: ^1.0.4 - path: ^1.6.4 - path_provider_linux: ^0.0.1 - shared_preferences_platform_interface: ^1.0.0 + file: ^6.0.0 + meta: ^1.3.0 + path: ^1.8.0 + path_provider_linux: ^2.0.0 + shared_preferences_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart b/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart index 8c659f212aa5..cf0bc80e3ec2 100644 --- a/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart +++ b/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart @@ -7,9 +7,9 @@ import 'package:path/path.dart' as path; import 'package:path_provider_linux/path_provider_linux.dart'; import 'package:shared_preferences_linux/shared_preferences_linux.dart'; -MemoryFileSystem fs; - void main() { + late MemoryFileSystem fs; + setUp(() { fs = MemoryFileSystem.test(); }); @@ -19,7 +19,7 @@ void main() { Future _getFilePath() async { final pathProvider = PathProviderLinux(); final directory = await pathProvider.getApplicationSupportPath(); - return path.join(directory, 'shared_preferences.json'); + return path.join(directory!, 'shared_preferences.json'); } _writeTestFile(String value) async { diff --git a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md index 177f1f2e02e2..00c2f628796f 100644 --- a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.0 + +* Migrate to null safety. + +## 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/README.md b/packages/shared_preferences/shared_preferences_macos/README.md index c2949c9f5d33..170a8270c402 100644 --- a/packages/shared_preferences/shared_preferences_macos/README.md +++ b/packages/shared_preferences/shared_preferences_macos/README.md @@ -2,13 +2,6 @@ The macos implementation of [`shared_preferences`][1]. -**Please set your constraint to `shared_preferences_macos: '>=0.0.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. -Please use `shared_preferences_macos: '>=0.0.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package diff --git a/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart index 0d49ed95dd2d..58b59463b352 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart @@ -1,12 +1,18 @@ +// Copyright 2017, 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. + +// @dart=2.9 + import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('$SharedPreferences', () { + group('SharedPreferencesMacOS', () { const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, @@ -23,67 +29,81 @@ void main() { 'flutter.List': ['baz', 'quox'], }; - SharedPreferences preferences; + SharedPreferencesStorePlatform preferences; setUp(() async { - preferences = await SharedPreferences.getInstance(); + preferences = SharedPreferencesStorePlatform.instance; }); tearDown(() { preferences.clear(); }); - test('reading', () async { - expect(preferences.get('String'), isNull); - expect(preferences.get('bool'), isNull); - expect(preferences.get('int'), isNull); - expect(preferences.get('double'), isNull); - expect(preferences.get('List'), isNull); - expect(preferences.getString('String'), isNull); - expect(preferences.getBool('bool'), isNull); - expect(preferences.getInt('int'), isNull); - expect(preferences.getDouble('double'), isNull); - expect(preferences.getStringList('List'), isNull); + // Normally the app-facing package adds the prefix, but since this test + // bypasses the app-facing package it needs to be manually added. + String _prefixedKey(String key) { + return 'flutter.$key'; + } + + testWidgets('reading', (WidgetTester _) async { + final Map values = await preferences.getAll(); + expect(values[_prefixedKey('String')], isNull); + expect(values[_prefixedKey('bool')], isNull); + expect(values[_prefixedKey('int')], isNull); + expect(values[_prefixedKey('double')], isNull); + expect(values[_prefixedKey('List')], isNull); }); - test('writing', () async { + testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ - preferences.setString('String', kTestValues2['flutter.String']), - preferences.setBool('bool', kTestValues2['flutter.bool']), - preferences.setInt('int', kTestValues2['flutter.int']), - preferences.setDouble('double', kTestValues2['flutter.double']), - preferences.setStringList('List', kTestValues2['flutter.List']) + preferences.setValue( + 'String', _prefixedKey('String'), kTestValues2['flutter.String']), + preferences.setValue( + 'Bool', _prefixedKey('bool'), kTestValues2['flutter.bool']), + preferences.setValue( + 'Int', _prefixedKey('int'), kTestValues2['flutter.int']), + preferences.setValue( + 'Double', _prefixedKey('double'), kTestValues2['flutter.double']), + preferences.setValue( + 'StringList', _prefixedKey('List'), kTestValues2['flutter.List']) ]); - expect(preferences.getString('String'), kTestValues2['flutter.String']); - expect(preferences.getBool('bool'), kTestValues2['flutter.bool']); - expect(preferences.getInt('int'), kTestValues2['flutter.int']); - expect(preferences.getDouble('double'), kTestValues2['flutter.double']); - expect(preferences.getStringList('List'), kTestValues2['flutter.List']); + final Map values = await preferences.getAll(); + expect(values[_prefixedKey('String')], kTestValues2['flutter.String']); + expect(values[_prefixedKey('bool')], kTestValues2['flutter.bool']); + expect(values[_prefixedKey('int')], kTestValues2['flutter.int']); + expect(values[_prefixedKey('double')], kTestValues2['flutter.double']); + expect(values[_prefixedKey('List')], kTestValues2['flutter.List']); }); - test('removing', () async { - const String key = 'testKey'; - await preferences.setString(key, kTestValues['flutter.String']); - await preferences.setBool(key, kTestValues['flutter.bool']); - await preferences.setInt(key, kTestValues['flutter.int']); - await preferences.setDouble(key, kTestValues['flutter.double']); - await preferences.setStringList(key, kTestValues['flutter.List']); + testWidgets('removing', (WidgetTester _) async { + final String key = _prefixedKey('testKey'); + await preferences.setValue('String', key, kTestValues['flutter.String']); + await preferences.setValue('Bool', key, kTestValues['flutter.bool']); + await preferences.setValue('Int', key, kTestValues['flutter.int']); + await preferences.setValue('Double', key, kTestValues['flutter.double']); + await preferences.setValue( + 'StringList', key, kTestValues['flutter.List']); await preferences.remove(key); - expect(preferences.get('testKey'), isNull); + final Map values = await preferences.getAll(); + expect(values[key], isNull); }); - test('clearing', () async { - await preferences.setString('String', kTestValues['flutter.String']); - await preferences.setBool('bool', kTestValues['flutter.bool']); - await preferences.setInt('int', kTestValues['flutter.int']); - await preferences.setDouble('double', kTestValues['flutter.double']); - await preferences.setStringList('List', kTestValues['flutter.List']); + testWidgets('clearing', (WidgetTester _) async { + await preferences.setValue( + 'String', 'String', kTestValues['flutter.String']); + await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']); + await preferences.setValue('Int', 'int', kTestValues['flutter.int']); + await preferences.setValue( + 'Double', 'double', kTestValues['flutter.double']); + await preferences.setValue( + 'StringList', 'List', kTestValues['flutter.List']); await preferences.clear(); - expect(preferences.getString('String'), null); - expect(preferences.getBool('bool'), null); - expect(preferences.getInt('int'), null); - expect(preferences.getDouble('double'), null); - expect(preferences.getStringList('List'), null); + final Map values = await preferences.getAll(); + expect(values['String'], null); + expect(values['bool'], null); + expect(values['int'], null); + expect(values['double'], null); + expect(values['List'], null); }); }); } diff --git a/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart b/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart index 46daeff6706f..f1058cddd63b 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart @@ -7,7 +7,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { runApp(MyApp()); @@ -24,22 +24,28 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key key}) : super(key: key); + SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); } class SharedPreferencesDemoState extends State { - Future _prefs = SharedPreferences.getInstance(); - Future _counter; + SharedPreferencesStorePlatform _prefs = + SharedPreferencesStorePlatform.instance; + late Future _counter; + + // Includes the prefix because this is using the platform interface directly, + // but the prefix (which the native code assumes is present) is added by the + // app-facing package. + static const String _prefKey = 'flutter.counter'; Future _incrementCounter() async { - final SharedPreferences prefs = await _prefs; - final int counter = (prefs.getInt('counter') ?? 0) + 1; + final Map values = await _prefs.getAll(); + final int counter = ((values[_prefKey] as int?) ?? 0) + 1; setState(() { - _counter = prefs.setInt("counter", counter).then((bool success) { + _counter = _prefs.setValue('Int', _prefKey, counter).then((bool success) { return counter; }); }); @@ -48,8 +54,8 @@ class SharedPreferencesDemoState extends State { @override void initState() { super.initState(); - _counter = _prefs.then((SharedPreferences prefs) { - return (prefs.getInt('counter') ?? 0); + _counter = _prefs.getAll().then((Map values) { + return (values[_prefKey] as int?) ?? 0; }); } diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/project.pbxproj index a95e62daada1..20c47b4f601f 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -26,10 +26,6 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DD4A1B9DEDBB72C87CD7AE27 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5067D74CB28D28AE3B3DD05B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -50,8 +46,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -70,7 +64,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; @@ -80,7 +73,6 @@ 899489AD6AA35AECA4E2BEA6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; B36FDC1D769C9045B8821207 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,8 +80,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, DD4A1B9DEDBB72C87CD7AE27 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -145,8 +135,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -281,7 +269,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -308,10 +296,13 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/shared_preferences_macos/shared_preferences_macos.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_macos.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..21a3cc14c74e 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml index 93d5d42597cd..7db361fccfd5 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml @@ -4,8 +4,13 @@ description: Demonstrates how to use the shared_preferences plugin. dependencies: flutter: sdk: flutter - shared_preferences: any + shared_preferences_platform_interface: ^2.0.0 shared_preferences_macos: + # When depending on this package from a real application you should use: + # shared_preferences_macos: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -13,11 +18,11 @@ dev_dependencies: sdk: flutter integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.8" diff --git a/packages/shared_preferences/shared_preferences_macos/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_macos/example/test_driver/integration_test.dart index 7a2c21338786..ac106b63b339 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/test_driver/integration_test.dart +++ b/packages/shared_preferences/shared_preferences_macos/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/shared_preferences/shared_preferences_macos/ios/shared_preferences_macos.podspec b/packages/shared_preferences/shared_preferences_macos/ios/shared_preferences_macos.podspec deleted file mode 100644 index 8e2a2bd30dac..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/ios/shared_preferences_macos.podspec +++ /dev/null @@ -1,21 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'shared_preferences_macos' - s.version = '0.0.1' - s.summary = 'No-op implementation of shared_preferences desktop plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of shared_preferences to avoid build issues on iOS. - DESC - - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_macos' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end diff --git a/packages/shared_preferences/shared_preferences_macos/pubspec.yaml b/packages/shared_preferences/shared_preferences_macos/pubspec.yaml index b161327e3f3d..24811830c1fd 100644 --- a/packages/shared_preferences/shared_preferences_macos/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_macos/pubspec.yaml @@ -1,9 +1,6 @@ name: shared_preferences_macos 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: 2.0.0 homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_macos flutter: @@ -13,12 +10,13 @@ flutter: pluginClass: SharedPreferencesPlugin environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" dependencies: - shared_preferences_platform_interface: ^1.0.0 + shared_preferences_platform_interface: ^2.0.0 flutter: sdk: flutter + dev_dependencies: - pedantic: ^1.8.0 + pedantic: ^1.10.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..b402f6e57e88 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.0 + +* Migrate to null safety. + +## 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/lib/method_channel_shared_preferences.dart b/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart index 66009a5caf14..c02c537adcbd 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart @@ -18,39 +18,32 @@ const MethodChannel _kChannel = class MethodChannelSharedPreferencesStore extends SharedPreferencesStorePlatform { @override - Future remove(String key) { - return _invokeBoolMethod('remove', { - 'key': key, - }); + Future remove(String key) async { + return (await _kChannel.invokeMethod( + 'remove', + {'key': key}, + ))!; } @override - Future setValue(String valueType, String key, Object value) { - return _invokeBoolMethod('set$valueType', { - 'key': key, - 'value': value, - }); - } - - Future _invokeBoolMethod(String method, Map params) { - return _kChannel - .invokeMethod(method, params) - // TODO(yjbanov): I copied this from the original - // shared_preferences.dart implementation, but I - // actually do not know why it's necessary to pipe the - // result through an identity function. - // - // Source: https://github.com/flutter/plugins/blob/3a87296a40a2624d200917d58f036baa9fb18df8/packages/shared_preferences/lib/shared_preferences.dart#L134 - .then((dynamic result) => result); + Future setValue(String valueType, String key, Object value) async { + return (await _kChannel.invokeMethod( + 'set$valueType', + {'key': key, 'value': value}, + ))!; } @override - Future clear() { - return _kChannel.invokeMethod('clear'); + Future clear() async { + return (await _kChannel.invokeMethod('clear'))!; } @override - Future> getAll() { - return _kChannel.invokeMapMethod('getAll'); + Future> getAll() async { + final Map? preferences = + await _kChannel.invokeMapMethod('getAll'); + + if (preferences == null) return {}; + return preferences; } } diff --git a/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart b/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart index 5a2b99ca69b1..cf194f82c267 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart @@ -4,7 +4,7 @@ import 'dart:async'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart'; import 'method_channel_shared_preferences.dart'; diff --git a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml index 3b8f2bfcada4..85c64a030036 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml @@ -1,18 +1,17 @@ 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: 2.0.0 dependencies: - meta: ^1.0.4 flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.8" diff --git a/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart index 4cc79b058675..d828126168ba 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart @@ -15,7 +15,7 @@ void main() { 'plugins.flutter.io/shared_preferences', ); - const Map kTestValues = { + const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.Bool': true, 'flutter.Int': 42, @@ -23,10 +23,10 @@ void main() { 'flutter.StringList': ['foo', 'bar'], }; - InMemorySharedPreferencesStore testData; + late InMemorySharedPreferencesStore testData; final List log = []; - MethodChannelSharedPreferencesStore store; + late MethodChannelSharedPreferencesStore store; setUp(() async { testData = InMemorySharedPreferencesStore.empty(); @@ -44,9 +44,9 @@ void main() { return await testData.clear(); } final RegExp setterRegExp = RegExp(r'set(.*)'); - final Match match = setterRegExp.matchAsPrefix(methodCall.method); - if (match.groupCount == 1) { - final String valueType = match.group(1); + final Match? match = setterRegExp.matchAsPrefix(methodCall.method); + if (match?.groupCount == 1) { + final String valueType = match!.group(1)!; final String key = methodCall.arguments['key']; final Object value = methodCall.arguments['value']; return await testData.setValue(valueType, key, value); @@ -59,8 +59,6 @@ void main() { tearDown(() async { await testData.clear(); - store = null; - testData = null; }); test('getAll', () async { diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index b2076dd8c17e..ec08267fe59f 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.0 + +* Migrate to null-safety. + +## 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/README.md b/packages/shared_preferences/shared_preferences_web/README.md index 2bb8a9316d86..8f9d22d86ef5 100644 --- a/packages/shared_preferences/shared_preferences_web/README.md +++ b/packages/shared_preferences/shared_preferences_web/README.md @@ -2,13 +2,6 @@ The web implementation of [`shared_preferences`][1]. -**Please set your constraint to `shared_preferences_web: '>=0.1.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.1.y+z`. -Please use `shared_preferences_web: '>=0.1.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package diff --git a/packages/shared_preferences/shared_preferences_web/ios/shared_preferences_web.podspec b/packages/shared_preferences/shared_preferences_web/ios/shared_preferences_web.podspec deleted file mode 100644 index 11f8b73e02d8..000000000000 --- a/packages/shared_preferences/shared_preferences_web/ios/shared_preferences_web.podspec +++ /dev/null @@ -1,20 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'shared_preferences_web' - s.version = '0.0.1' - s.summary = 'No-op implementation of shared_preferences web plugin to avoid build issues on iOS' - s.description = <<-DESC -temp fake shared_preferences_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end diff --git a/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart index 8a0f137ddcc8..346877e8a120 100644 --- a/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart +++ b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart @@ -14,7 +14,7 @@ import 'package:shared_preferences_platform_interface/shared_preferences_platfor /// This class implements the `package:shared_preferences` functionality for the web. class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { /// Registers this class as the default instance of [SharedPreferencesStorePlatform]. - static void registerWith(Registrar registrar) { + static void registerWith(Registrar? registrar) { SharedPreferencesStorePlatform.instance = SharedPreferencesPlugin(); } @@ -31,9 +31,9 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { @override Future> getAll() async { - final Map allData = {}; + final Map allData = {}; for (String key in _storedFlutterKeys) { - allData[key] = _decodeValue(html.window.localStorage[key]); + allData[key] = _decodeValue(html.window.localStorage[key]!); } return allData; } @@ -46,7 +46,7 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { } @override - Future setValue(String valueType, String key, Object value) async { + Future setValue(String valueType, String key, Object? value) async { _checkPrefix(key); html.window.localStorage[key] = _encodeValue(value); return true; @@ -62,17 +62,12 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { } } - List get _storedFlutterKeys { - final List keys = []; - for (String key in html.window.localStorage.keys) { - if (key.startsWith('flutter.')) { - keys.add(key); - } - } - return keys; + Iterable get _storedFlutterKeys { + return html.window.localStorage.keys + .where((key) => key.startsWith('flutter.')); } - String _encodeValue(Object value) { + String _encodeValue(Object? value) { return json.encode(value); } diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml index a153135b6da7..9a14b218572a 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -1,10 +1,7 @@ name: shared_preferences_web description: Web platform implementation of shared_preferences homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_web -# 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: 2.0.0 flutter: plugin: @@ -14,18 +11,18 @@ flutter: fileName: shared_preferences_web.dart dependencies: - shared_preferences_platform_interface: ^1.0.0 + shared_preferences_platform_interface: ^2.0.0 flutter: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 + meta: ^1.3.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart b/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart index 951f04cbce5a..c0cf92cc1bf0 100644 --- a/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart +++ b/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart @@ -2,12 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@TestOn('chrome') // Uses web-only Flutter SDK - +@TestOn('chrome') import 'dart:convert' show json; import 'dart:html' as html; import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:shared_preferences_web/shared_preferences_web.dart'; @@ -26,6 +26,8 @@ void main() { }); test('registers itself', () { + SharedPreferencesStorePlatform.instance = + MethodChannelSharedPreferencesStore(); expect(SharedPreferencesStorePlatform.instance, isNot(isA())); SharedPreferencesPlugin.registerWith(null); diff --git a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md index 8604db297ca6..6fa4eb162083 100644 --- a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -1,3 +1,23 @@ +## 2.0.0 + +* Migrate to null-safety. + +## 0.0.2+3 + +* Remove 'ffi' dependency. + +## 0.0.2+2 + +* Relax 'ffi' version constraint. + +## 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/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/example/CHANGELOG.md deleted file mode 100644 index 41cc7d8192ec..000000000000 --- a/packages/shared_preferences/shared_preferences_windows/example/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. 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..027daa6eaeb1 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 @@ -2,13 +2,15 @@ // 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 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences_windows/shared_preferences_windows.dart'; -import 'package:e2e/e2e.dart'; +import 'package:integration_test/integration_test.dart'; void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferencesWindows', () { const Map kTestValues = { @@ -37,7 +39,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 +48,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 +66,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 +79,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/example/lib/main.dart b/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart index 140851c90504..f0dc155aee4a 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart @@ -24,7 +24,7 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key key}) : super(key: key); + SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); @@ -32,14 +32,14 @@ class SharedPreferencesDemo extends StatefulWidget { class SharedPreferencesDemoState extends State { final prefs = SharedPreferencesWindows.instance; - Future _counter; + late Future _counter; Future _incrementCounter() async { final values = await prefs.getAll(); - final int counter = (values['counter'] as int ?? 0) + 1; + final int counter = (values['counter'] as int? ?? 0) + 1; setState(() { - _counter = prefs.setValue(null, "counter", counter).then((bool success) { + _counter = prefs.setValue('Int', 'counter', counter).then((bool success) { return counter; }); }); @@ -49,7 +49,7 @@ class SharedPreferencesDemoState extends State { void initState() { super.initState(); _counter = prefs.getAll().then((Map values) { - return (values['counter'] ?? 0); + return (values['counter'] as int? ?? 0); }); } diff --git a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml index 8a44e7874cfb..6725259c4bdc 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml @@ -2,22 +2,29 @@ name: shared_preferences_windows_example description: Demonstrates how to use the shared_preferences_windows plugin. environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - shared_preferences_windows: ^0.0.1 + shared_preferences_windows: ^2.0.0 dependency_overrides: shared_preferences_windows: + # When depending on this package from a real application you should use: + # shared_preferences_windows: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: flutter_driver: sdk: flutter - e2e: ^0.2.0 - pedantic: ^1.8.0 + integration_test: + path: ../../../integration_test + pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/shared_preferences/shared_preferences_windows/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_windows/example/test_driver/integration_test.dart index 102dfdfef708..353548ee0e8c 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/test_driver/integration_test.dart +++ b/packages/shared_preferences/shared_preferences_windows/example/test_driver/integration_test.dart @@ -2,14 +2,18 @@ // 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'; import 'package:flutter_driver/flutter_driver.dart'; Future main() async { final FlutterDriver driver = await FlutterDriver.connect(); - final String result = + final String data = await driver.requestData(null, timeout: const Duration(minutes: 1)); await driver.close(); - exit(result == 'pass' ? 0 : 1); + final Map result = jsonDecode(data); + exit(result['result'] == 'true' ? 0 : 1); } diff --git a/packages/shared_preferences/shared_preferences_windows/ios/shared_preferences_windows.podspec b/packages/shared_preferences/shared_preferences_windows/ios/shared_preferences_windows.podspec deleted file mode 100644 index 2e239e607493..000000000000 --- a/packages/shared_preferences/shared_preferences_windows/ios/shared_preferences_windows.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# Run `pod lib lint shared_preferences_windows.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'shared_preferences_windows' - s.version = '0.0.1' - s.summary = 'shared_preferences_windows iOS stub' - s.description = <<-DESC - No-op implementation of the windows shared_preferences plugin to avoid build issues on iOS - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_windows' } - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart index dd9ab8a0c38f..b2678c49782b 100644 --- a/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart +++ b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart @@ -27,42 +27,51 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { PathProviderWindows pathProvider = PathProviderWindows(); /// Local copy of preferences - Map _cachedPreferences; + Map? _cachedPreferences; /// Cached file for storing preferences. - File _localDataFilePath; + File? _localDataFilePath; /// Gets the file where the preferences are stored. - Future _getLocalDataFile() async { - if (_localDataFilePath == null) { - final directory = await pathProvider.getApplicationSupportPath(); - _localDataFilePath = - fs.file(path.join(directory, 'shared_preferences.json')); + Future _getLocalDataFile() async { + if (_localDataFilePath != null) { + return _localDataFilePath!; } - return _localDataFilePath; + final directory = await pathProvider.getApplicationSupportPath(); + if (directory == null) { + return null; + } + return _localDataFilePath = + fs.file(path.join(directory, 'shared_preferences.json')); } /// Gets the preferences from the stored file. Once read, the preferences are /// maintained in memory. Future> _readPreferences() async { - if (_cachedPreferences == null) { - _cachedPreferences = {}; - File localDataFile = await _getLocalDataFile(); - if (localDataFile.existsSync()) { - String stringMap = localDataFile.readAsStringSync(); - if (stringMap.isNotEmpty) { - _cachedPreferences = json.decode(stringMap) as Map; - } + if (_cachedPreferences != null) { + return _cachedPreferences!; + } + Map preferences = {}; + final File? localDataFile = await _getLocalDataFile(); + if (localDataFile != null && localDataFile.existsSync()) { + String stringMap = localDataFile.readAsStringSync(); + if (stringMap.isNotEmpty) { + preferences = json.decode(stringMap).cast(); } } - return _cachedPreferences; + _cachedPreferences = preferences; + return preferences; } /// Writes the cached preferences to disk. Returns [true] if the operation /// succeeded. Future _writePreferences(Map preferences) async { try { - File localDataFile = await _getLocalDataFile(); + final File? localDataFile = await _getLocalDataFile(); + if (localDataFile == null) { + print("Unable to determine where to write preferences."); + return false; + } if (!localDataFile.existsSync()) { localDataFile.createSync(recursive: true); } diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml index 0de70737bc19..d804dd1ddb8f 100644 --- a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml @@ -1,7 +1,8 @@ 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: 2.0.0 + flutter: plugin: @@ -11,21 +12,20 @@ flutter: pluginClass: none environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: '>=2.12.0-259.9.beta <3.0.0' + flutter: ">=1.20.0" dependencies: - shared_preferences_platform_interface: ^1.0.0 + shared_preferences_platform_interface: ^2.0.0 flutter: sdk: flutter - ffi: ^0.1.3 - file: ">=5.1.0 <7.0.0" - meta: ^1.1.7 - path: ^1.6.4 - path_provider_platform_interface: ^1.0.3 - path_provider_windows: ^0.0.2 + file: ^6.0.0 + meta: ^1.3.0 + path: ^1.8.0 + path_provider_platform_interface: ^2.0.0 + path_provider_windows: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart b/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart index b0827ca3b36b..785092f6fa16 100644 --- a/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart +++ b/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart @@ -10,8 +10,8 @@ import 'package:path_provider_windows/path_provider_windows.dart'; import 'package:shared_preferences_windows/shared_preferences_windows.dart'; void main() { - MemoryFileSystem fileSystem; - PathProviderWindows pathProvider; + late MemoryFileSystem fileSystem; + late PathProviderWindows pathProvider; setUp(() { fileSystem = MemoryFileSystem.test(); @@ -22,7 +22,7 @@ void main() { Future _getFilePath() async { final directory = await pathProvider.getApplicationSupportPath(); - return path.join(directory, 'shared_preferences.json'); + return path.join(directory!, 'shared_preferences.json'); } _writeTestFile(String value) async { @@ -87,23 +87,23 @@ void main() { /// path it returns is a root path that does not actually exist on Windows. class FakePathProviderWindows extends PathProviderPlatform implements PathProviderWindows { - VersionInfoQuerier versionInfoQuerier; + late VersionInfoQuerier versionInfoQuerier; @override - Future getApplicationSupportPath() async => r'C:\appsupport'; + Future getApplicationSupportPath() async => r'C:\appsupport'; @override - Future getTemporaryPath() async => null; + Future getTemporaryPath() async => null; @override - Future getLibraryPath() async => null; + Future getLibraryPath() async => null; @override - Future getApplicationDocumentsPath() async => null; + Future getApplicationDocumentsPath() async => null; @override - Future getDownloadsPath() async => null; + Future getDownloadsPath() async => null; @override - Future getPath(String folderID) async => null; + Future getPath(String folderID) async => ''; } diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 832c0c62f553..2d188366dfa5 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,27 @@ +## 6.0.1 + +* Update result to `True` on iOS when the url was loaded successfully. +* Added a README note about required applications. + +## 6.0.0 + +* Migrate to null safety. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Correct statement in description about which platforms url_launcher supports. + +## 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..daf21738d9b7 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -1,8 +1,8 @@ # 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 +A Flutter plugin for launching a URL. Supports iOS, Android, web, Windows, macOS, and Linux. ## Usage @@ -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. @@ -54,6 +51,10 @@ Common schemes supported by both iOS and Android: More details can be found here for [iOS](https://developer.apple.com/library/content/featuredarticles/iPhoneURLScheme_Reference/Introduction/Introduction.html) and [Android](https://developer.android.com/guide/components/intents-common.html) +**Note**: URL schemes are only supported if there are apps installed on the device that can +support them. For example, iOS simulators don't have a default email or phone +apps installed, so can't open `tel:` or `mailto:` links. + ### Encoding URLs URLs must be properly encoded, especially when including spaces or other special characters. This can be done using the [`Uri` class](https://api.dart.dev/stable/2.7.1/dart-core/Uri-class.html): @@ -83,7 +84,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..5f313f3870c5 100644 --- a/packages/url_launcher/url_launcher/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher/example/pubspec.yaml @@ -5,6 +5,11 @@ dependencies: flutter: sdk: flutter url_launcher: + # When depending on this package from a real application you should use: + # url_launcher: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -12,13 +17,13 @@ dev_dependencies: path: ../../../integration_test flutter_driver: sdk: flutter - pedantic: ^1.8.0 - mockito: ^4.1.1 - plugin_platform_interface: ^1.0.0 + pedantic: ^1.10.0 + mockito: ^5.0.0-nullsafety.7 + plugin_platform_interface: ">=1.0.0 <3.0.0" flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" 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/ios/Classes/FLTURLLauncherPlugin.m b/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m index 39013b3ca039..ac05417473a3 100644 --- a/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m +++ b/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m @@ -34,7 +34,7 @@ - (instancetype)initWithUrl:url withFlutterResult:result { - (void)safariViewController:(SFSafariViewController *)controller didCompleteInitialLoad:(BOOL)didLoadSuccessfully API_AVAILABLE(ios(9.0)) { if (didLoadSuccessfully) { - self.flutterResult(nil); + self.flutterResult(@YES); } else { self.flutterResult([FlutterError errorWithCode:@"Error" 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..4036748a2d2e 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -1,8 +1,8 @@ name: url_launcher -description: Flutter plugin for launching a URL on Android and iOS. Supports +description: Flutter plugin for launching a URL. 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.1 flutter: plugin: @@ -24,25 +24,25 @@ flutter: dependencies: flutter: sdk: flutter - url_launcher_platform_interface: ^1.0.9 + url_launcher_platform_interface: ^2.0.0 # 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: ^2.0.0 + url_launcher_macos: ^2.0.0 + url_launcher_windows: ^2.0.0 + url_launcher_web: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - test: ^1.3.0 - mockito: ^4.1.1 - plugin_platform_interface: ^1.0.0 - pedantic: ^1.8.0 + test: ^1.16.3 + mockito: ^5.0.0-nullsafety.7 + plugin_platform_interface: ">=1.0.0 <3.0.0" + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <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..bb3fd2ad92b5 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( @@ -193,6 +191,38 @@ void main() { throwsA(isA())); }); + test('send e-mail', () async { + await launch('mailto:gmail-noreply@google.com?subject=Hello'); + expect( + verify(await mock.launch( + any, + useSafariVC: anyNamed('useSafariVC'), + useWebView: anyNamed('useWebView'), + enableJavaScript: anyNamed('enableJavaScript'), + enableDomStorage: anyNamed('enableDomStorage'), + universalLinksOnly: anyNamed('universalLinksOnly'), + headers: anyNamed('headers'), + )), + isInstanceOf(), + ); + }); + + test('cannot send e-mail with forceSafariVC: true', () async { + expect( + () async => await launch( + 'mailto:gmail-noreply@google.com?subject=Hello', + forceSafariVC: true), + throwsA(isA())); + }); + + test('cannot send e-mail with forceWebView: true', () async { + expect( + () async => await launch( + 'mailto:gmail-noreply@google.com?subject=Hello', + forceWebView: true), + throwsA(isA())); + }); + test('controls system UI when changing statusBarBrightness', () async { final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); diff --git a/packages/url_launcher/url_launcher_linux/CHANGELOG.md b/packages/url_launcher/url_launcher_linux/CHANGELOG.md index e0a01ec629f5..ec9fad53437c 100644 --- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -1,3 +1,18 @@ +## 2.0.0 + +* Migrate to null safety. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Set `implementation` in pubspec.yaml + +## 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..e1008fddd4e1 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 @@ -2,22 +2,21 @@ // 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:url_launcher/url_launcher.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - test('canLaunch', () async { - expect(await canLaunch('randomstring'), false); - - // Generally all devices should have some default browser. - expect(await canLaunch('http://flutter.dev'), true); + testWidgets('canLaunch', (WidgetTester _) async { + UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; - // Desktop will not necessarily support sms:. + expect(await launcher.canLaunch('randomstring'), false); - // tel: and mailto: links may not be openable on every device. iOS - // simulators notably can't open these link types. + // Generally all devices should have some default browser. + expect(await launcher.canLaunch('http://flutter.dev'), true); }); } 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..f49e9fa290c5 100644 --- a/packages/url_launcher/url_launcher_linux/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_linux/example/lib/main.dart @@ -6,7 +6,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { runApp(MyApp()); @@ -26,7 +26,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 @@ -34,77 +34,24 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - Future _launched; - String _phone = ''; + Future? _launched; Future _launchInBrowser(String url) async { - if (await canLaunch(url)) { - await launch( + if (await UrlLauncherPlatform.instance.canLaunch(url)) { + await UrlLauncherPlatform.instance.launch( url, - forceSafariVC: false, - forceWebView: false, - headers: {'my_header_key': 'my_header_value'}, + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, ); } else { throw 'Could not launch $url'; } } - Future _launchInWebViewOrVC(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - headers: {'my_header_key': 'my_header_value'}, - ); - } else { - throw 'Could not launch $url'; - } - } - - Future _launchInWebViewWithJavaScript(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - enableJavaScript: true, - ); - } else { - throw 'Could not launch $url'; - } - } - - Future _launchInWebViewWithDomStorage(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - enableDomStorage: true, - ); - } else { - throw 'Could not launch $url'; - } - } - - Future _launchUniversalLinkIos(String url) async { - if (await canLaunch(url)) { - final bool nativeAppLaunchSucceeded = await launch( - url, - forceSafariVC: false, - universalLinksOnly: true, - ); - if (!nativeAppLaunchSucceeded) { - await launch( - url, - forceSafariVC: true, - ); - } - } - } - Widget _launchStatus(BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); @@ -113,14 +60,6 @@ class _MyHomePageState extends State { } } - Future _makePhoneCall(String url) async { - if (await canLaunch(url)) { - await launch(url); - } else { - throw 'Could not launch $url'; - } - } - @override Widget build(BuildContext context) { const String toLaunch = 'https://www.cylog.org/headers/'; @@ -133,57 +72,17 @@ class _MyHomePageState extends State { Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: TextField( - onChanged: (String text) => _phone = text, - decoration: const InputDecoration( - hintText: 'Input the phone number to launch')), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _makePhoneCall('tel:$_phone'); - }), - child: const Text('Make phone call'), - ), const Padding( 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( - onPressed: () => setState(() { - _launched = _launchInWebViewOrVC(toLaunch); - }), - child: const Text('Launch in app'), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewWithJavaScript(toLaunch); - }), - child: const Text('Launch in app(JavaScript ON)'), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewWithDomStorage(toLaunch); - }), - child: const Text('Launch in app(DOM storage ON)'), - ), - const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchUniversalLinkIos(toLaunch); - }), - child: const Text( - 'Launch a universal link in a native app, fallback to Safari.(Youtube)'), - ), - const Padding(padding: EdgeInsets.all(16.0)), FutureBuilder(future: _launched, builder: _launchStatus), ], ), diff --git a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml index 9604637c336c..63c920fba614 100644 --- a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml @@ -4,20 +4,25 @@ description: Demonstrates how to use the url_launcher plugin. dependencies: flutter: sdk: flutter - url_launcher: any url_launcher_linux: + # When depending on this package from a real application you should use: + # url_launcher_linux: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ + url_launcher_platform_interface: ^2.0.0 dev_dependencies: integration_test: path: ../../../integration_test flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/url_launcher/url_launcher_linux/example/test_driver/integration_test.dart b/packages/url_launcher/url_launcher_linux/example/test_driver/integration_test.dart index 7a2c21338786..a8a56aa90f6a 100644 --- a/packages/url_launcher/url_launcher_linux/example/test_driver/integration_test.dart +++ b/packages/url_launcher/url_launcher_linux/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/url_launcher/url_launcher_linux/ios/.gitignore b/packages/url_launcher/url_launcher_linux/ios/.gitignore deleted file mode 100644 index aa479fd3ce8a..000000000000 --- a/packages/url_launcher/url_launcher_linux/ios/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/url_launcher/url_launcher_linux/ios/url_launcher_linux.podspec b/packages/url_launcher/url_launcher_linux/ios/url_launcher_linux.podspec deleted file mode 100644 index 1359fd403d8d..000000000000 --- a/packages/url_launcher/url_launcher_linux/ios/url_launcher_linux.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint url_launcher_linux.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'url_launcher_linux' - s.version = '0.0.1' - s.summary = 'url_launcher_linux iOS stub' - s.description = <<-DESC - No-op implementation of the Linux url_launcher plugin to avoid build issues on iOS - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_linux' } - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml index 16448e4020e5..cc974094b2d0 100644 --- a/packages/url_launcher/url_launcher_linux/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml @@ -1,17 +1,18 @@ name: url_launcher_linux description: Linux implementation of the url_launcher plugin. -version: 0.0.1+4 +version: 2.0.0 homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_linux flutter: plugin: + implements: url_launcher platforms: linux: pluginClass: UrlLauncherPlugin environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md index 190aa2887353..6b0820fd5588 100644 --- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md @@ -1,3 +1,17 @@ +## 2.0.0 + +* Migrate to null safety. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Set `implementation` in pubspec.yaml + +## 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. @@ -38,4 +52,3 @@ # 0.0.1 * Initial open source release. - diff --git a/packages/url_launcher/url_launcher_macos/README.md b/packages/url_launcher/url_launcher_macos/README.md index e0c326ba86bf..28aa18817d6c 100644 --- a/packages/url_launcher/url_launcher_macos/README.md +++ b/packages/url_launcher/url_launcher_macos/README.md @@ -2,13 +2,6 @@ The macos implementation of [`url_launcher`][1]. -**Please set your constraint to `url_launcher_macos: '>=0.0.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. -Please use `url_launcher_macos: '>=0.0.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package 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..d0c1a8bd7325 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 @@ -2,23 +2,24 @@ // 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:url_launcher/url_launcher.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - test('canLaunch', () async { - expect(await canLaunch('randomstring'), false); + testWidgets('canLaunch', (WidgetTester _) async { + UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + + expect(await launcher.canLaunch('randomstring'), false); // Generally all devices should have some default browser. - expect(await canLaunch('http://flutter.dev'), true); + expect(await launcher.canLaunch('http://flutter.dev'), true); // Generally all devices should have some default SMS app. - expect(await canLaunch('sms:5555555555'), true); - - // tel: and mailto: links may not be openable on every device. iOS - // simulators notably can't open these link types. + expect(await launcher.canLaunch('sms:5555555555'), true); }); } diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Flutter/AppFrameworkInfo.plist b/packages/url_launcher/url_launcher_macos/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Flutter/Debug.xcconfig b/packages/url_launcher/url_launcher_macos/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 9803018ca79d..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Flutter/Release.xcconfig b/packages/url_launcher/url_launcher_macos/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index a4a8c604e13d..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_macos/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index db72809a6169..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 2D92223F1EC1DA93007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */; }; - 2E37D9A274B2EACB147AC51B /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 856D0913184F79C678A42603 /* libPods-Runner.a */; }; - 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, ); }; }; - 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 */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 2D92223D1EC1DA93007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GeneratedPluginRegistrant.h; path = Runner/GeneratedPluginRegistrant.h; sourceTree = ""; }; - 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GeneratedPluginRegistrant.m; path = Runner/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 = ""; }; - 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 = ""; }; - 836316F9AEA584411312E29F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 856D0913184F79C678A42603 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 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 = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A84BFEE343F54B983D1B67EB /* 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 = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 2E37D9A274B2EACB147AC51B /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { - isa = PBXGroup; - children = ( - 836316F9AEA584411312E29F /* Pods-Runner.debug.xcconfig */, - A84BFEE343F54B983D1B67EB /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 2D92223D1EC1DA93007564B0 /* GeneratedPluginRegistrant.h */, - 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */, - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 840012C8B5EDBCF56B0E4AC1 /* Pods */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 856D0913184F79C678A42603 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 2D92223F1EC1DA93007564B0 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.urlLauncher; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.urlLauncher; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/AppDelegate.h b/packages/url_launcher/url_launcher_macos/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,10 +0,0 @@ -// 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 -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/AppDelegate.m b/packages/url_launcher/url_launcher_macos/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 9cf1c7796c6a..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +0,0 @@ -// 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. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - [super application:application didFinishLaunchingWithOptions:launchOptions]; - return YES; -} - -@end diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index ebf48f603974..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Base.lproj/Main.storyboard b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Info.plist b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Info.plist deleted file mode 100644 index 80aec052fa79..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - url_launcher_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/main.m b/packages/url_launcher/url_launcher_macos/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// 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 -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} 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..f49e9fa290c5 100644 --- a/packages/url_launcher/url_launcher_macos/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_macos/example/lib/main.dart @@ -6,7 +6,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { runApp(MyApp()); @@ -26,7 +26,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 @@ -34,77 +34,24 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - Future _launched; - String _phone = ''; + Future? _launched; Future _launchInBrowser(String url) async { - if (await canLaunch(url)) { - await launch( + if (await UrlLauncherPlatform.instance.canLaunch(url)) { + await UrlLauncherPlatform.instance.launch( url, - forceSafariVC: false, - forceWebView: false, - headers: {'my_header_key': 'my_header_value'}, + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, ); } else { throw 'Could not launch $url'; } } - Future _launchInWebViewOrVC(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - headers: {'my_header_key': 'my_header_value'}, - ); - } else { - throw 'Could not launch $url'; - } - } - - Future _launchInWebViewWithJavaScript(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - enableJavaScript: true, - ); - } else { - throw 'Could not launch $url'; - } - } - - Future _launchInWebViewWithDomStorage(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - enableDomStorage: true, - ); - } else { - throw 'Could not launch $url'; - } - } - - Future _launchUniversalLinkIos(String url) async { - if (await canLaunch(url)) { - final bool nativeAppLaunchSucceeded = await launch( - url, - forceSafariVC: false, - universalLinksOnly: true, - ); - if (!nativeAppLaunchSucceeded) { - await launch( - url, - forceSafariVC: true, - ); - } - } - } - Widget _launchStatus(BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); @@ -113,14 +60,6 @@ class _MyHomePageState extends State { } } - Future _makePhoneCall(String url) async { - if (await canLaunch(url)) { - await launch(url); - } else { - throw 'Could not launch $url'; - } - } - @override Widget build(BuildContext context) { const String toLaunch = 'https://www.cylog.org/headers/'; @@ -133,57 +72,17 @@ class _MyHomePageState extends State { Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: TextField( - onChanged: (String text) => _phone = text, - decoration: const InputDecoration( - hintText: 'Input the phone number to launch')), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _makePhoneCall('tel:$_phone'); - }), - child: const Text('Make phone call'), - ), const Padding( 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( - onPressed: () => setState(() { - _launched = _launchInWebViewOrVC(toLaunch); - }), - child: const Text('Launch in app'), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewWithJavaScript(toLaunch); - }), - child: const Text('Launch in app(JavaScript ON)'), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewWithDomStorage(toLaunch); - }), - child: const Text('Launch in app(DOM storage ON)'), - ), - const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchUniversalLinkIos(toLaunch); - }), - child: const Text( - 'Launch a universal link in a native app, fallback to Safari.(Youtube)'), - ), - const Padding(padding: EdgeInsets.all(16.0)), FutureBuilder(future: _launched, builder: _launchStatus), ], ), diff --git a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml index dbf8951ff408..40bb4eaba67a 100644 --- a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml @@ -4,20 +4,25 @@ description: Demonstrates how to use the url_launcher plugin. dependencies: flutter: sdk: flutter - url_launcher: any url_launcher_macos: + # When depending on this package from a real application you should use: + # url_launcher_macos: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ + url_launcher_platform_interface: ^2.0.0 dev_dependencies: integration_test: path: ../../../integration_test flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/url_launcher/url_launcher_macos/example/test_driver/integration_test.dart b/packages/url_launcher/url_launcher_macos/example/test_driver/integration_test.dart index 7a2c21338786..a8a56aa90f6a 100644 --- a/packages/url_launcher/url_launcher_macos/example/test_driver/integration_test.dart +++ b/packages/url_launcher/url_launcher_macos/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/url_launcher/url_launcher_macos/ios/url_launcher_macos.podspec b/packages/url_launcher/url_launcher_macos/ios/url_launcher_macos.podspec deleted file mode 100644 index 2bfe79708555..000000000000 --- a/packages/url_launcher/url_launcher_macos/ios/url_launcher_macos.podspec +++ /dev/null @@ -1,21 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'url_launcher_macos' - s.version = '0.0.1' - s.summary = 'No-op implementation of the macos url_launcher plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of the macos url_launcher plugin to avoid build issues on iOS - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_macos' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end - diff --git a/packages/url_launcher/url_launcher_macos/pubspec.yaml b/packages/url_launcher/url_launcher_macos/pubspec.yaml index 1a77bc7aeab3..6b5e6cf2a825 100644 --- a/packages/url_launcher/url_launcher_macos/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/pubspec.yaml @@ -1,21 +1,19 @@ name: url_launcher_macos 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: 2.0.0 homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_macos flutter: plugin: + implements: url_launcher platforms: macos: pluginClass: UrlLauncherPlugin fileName: url_launcher_macos.dart environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" 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..d617e514035e 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.1 + +* Fix SDK range. + +## 2.0.0 + +* 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..d3ec0aafd126 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.1 dependencies: flutter: sdk: flutter - meta: ^1.0.5 - plugin_platform_interface: ^1.0.1 + plugin_platform_interface: ">=1.0.0 <3.0.0" dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 + mockito: ^5.0.0-nullsafety.7 + pedantic: ^1.10.0 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.22.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <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..a01637e2f378 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 @@ -16,18 +16,20 @@ final MethodCodec _codec = const JSONMethodCodec(); void main() { TestWidgetsFlutterBinding.ensureInitialized(); - PlatformMessageCallback oldHandler; - MethodCall lastCall; + PlatformMessageCallback? oldHandler; + MethodCall? lastCall; setUp(() { oldHandler = window.onPlatformMessage; window.onPlatformMessage = ( String name, - ByteData data, - PlatformMessageResponseCallback callback, + ByteData? data, + PlatformMessageResponseCallback? callback, ) { lastCall = _codec.decodeMethodCall(data); - callback(_codec.encodeSuccessEnvelope(true)); + if (callback != null) { + callback(_codec.encodeSuccessEnvelope(true)); + } }; }); 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..b5a96b18c91a 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 @@ -42,6 +42,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 +66,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 +280,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( @@ -289,5 +313,5 @@ class ImplementsUrlLauncherPlatform extends Mock class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform { @override - final LinkDelegate linkDelegate = null; + final LinkDelegate? linkDelegate = null; } diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index 093029104f8f..7da0ba0a8095 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,15 @@ +# 2.0.0 + +- Migrate to null safety. + +# 0.1.5+3 + +- Fix Link misalignment [issue](https://github.com/flutter/flutter/issues/70053). + +# 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 +49,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/example/README.md b/packages/url_launcher/url_launcher_web/example/README.md new file mode 100644 index 000000000000..b75df09c33f1 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/README.md @@ -0,0 +1,31 @@ +# Testing + +This package utilizes the `integration_test` package to run its tests in a web browser. + +See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. + +## Running the tests + +Make sure you have updated to the latest Flutter master. + +1. Check what version of Chrome is running on the machine you're running tests on. + +2. Download and install driver for that version from here: + * + +3. Start the driver using `chromedriver --port=4444` + +4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_test_driver.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): + + * Single: `./run_test.sh integration_test/TEST_NAME.dart` + * All: `./run_test.sh` + +## Mocks + +There's `.mocks.dart` files next to the test files that use them. + +They're [generated by Mockito](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#code-generation). + +Mocks might be manually re-generated with the following command: `flutter pub run build_runner build`. If there are any changes in the mocks, feel free to commit them. + +(Mocks will be auto-generated by the `run_test.sh` script as well.) diff --git a/packages/url_launcher/url_launcher_web/example/build.yaml b/packages/url_launcher/url_launcher_web/example/build.yaml new file mode 100644 index 000000000000..db3104bb04c6 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - integration_test/*.dart + - lib/$lib$ + - $package$ diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart b/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart new file mode 100644 index 000000000000..3c1dbd8b1b89 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart @@ -0,0 +1,150 @@ +// 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:html' as html; +import 'dart:js_util'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:url_launcher_platform_interface/link.dart'; +import 'package:url_launcher_web/src/link.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Link Widget', () { + testWidgets('creates anchor with correct attributes', + (WidgetTester tester) async { + final Uri uri = Uri.parse('http://foobar/example?q=1'); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: WebLinkDelegate(TestLinkInfo( + uri: uri, + target: LinkTarget.blank, + builder: (BuildContext context, FollowLink? followLink) { + return Container(width: 100, height: 100); + }, + )), + )); + // Platform view creation happens asynchronously. + await tester.pumpAndSettle(); + + final html.Element anchor = _findSingleAnchor(); + expect(anchor.getAttribute('href'), uri.toString()); + expect(anchor.getAttribute('target'), '_blank'); + + final Uri uri2 = Uri.parse('http://foobar2/example?q=2'); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: WebLinkDelegate(TestLinkInfo( + uri: uri2, + target: LinkTarget.self, + builder: (BuildContext context, FollowLink? followLink) { + return Container(width: 100, height: 100); + }, + )), + )); + await tester.pumpAndSettle(); + + // Check that the same anchor has been updated. + expect(anchor.getAttribute('href'), uri2.toString()); + expect(anchor.getAttribute('target'), '_self'); + }); + + testWidgets('sizes itself correctly', (WidgetTester tester) async { + final Key containerKey = GlobalKey(); + final Uri uri = Uri.parse('http://foobar'); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints.tight(Size(100.0, 100.0)), + child: WebLinkDelegate(TestLinkInfo( + uri: uri, + target: LinkTarget.blank, + builder: (BuildContext context, FollowLink? followLink) { + return Container( + key: containerKey, + child: SizedBox(width: 50.0, height: 50.0), + ); + }, + )), + ), + ), + )); + await tester.pumpAndSettle(); + + final Size containerSize = tester.getSize(find.byKey(containerKey)); + // The Stack widget inserted by the `WebLinkDelegate` shouldn't loosen the + // constraints before passing them to the inner container. So the inner + // container should respect the tight constraints given by the ancestor + // `ConstrainedBox` widget. + expect(containerSize.width, 100.0); + expect(containerSize.height, 100.0); + }); + + // See: https://github.com/flutter/plugins/pull/3522#discussion_r574703724 + testWidgets('uri can be null', (WidgetTester tester) async { + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: WebLinkDelegate(TestLinkInfo( + uri: null, + target: LinkTarget.defaultTarget, + builder: (BuildContext context, FollowLink? followLink) { + return Container(width: 100, height: 100); + }, + )), + )); + // Platform view creation happens asynchronously. + await tester.pumpAndSettle(); + + final html.Element anchor = _findSingleAnchor(); + expect(anchor.hasAttribute('href'), false); + }); + }); +} + +html.Element _findSingleAnchor() { + final List foundAnchors = []; + for (final html.Element anchor in html.document.querySelectorAll('a')) { + if (hasProperty(anchor, linkViewIdProperty)) { + foundAnchors.add(anchor); + } + } + + // Search inside platform views with shadow roots as well. + for (final html.Element platformView + in html.document.querySelectorAll('flt-platform-view')) { + final html.ShadowRoot shadowRoot = platformView.shadowRoot!; + if (shadowRoot != null) { + for (final html.Element anchor in shadowRoot.querySelectorAll('a')) { + if (hasProperty(anchor, linkViewIdProperty)) { + foundAnchors.add(anchor); + } + } + } + } + + return foundAnchors.single; +} + +class TestLinkInfo extends LinkInfo { + @override + final LinkWidgetBuilder builder; + + @override + final Uri? uri; + + @override + final LinkTarget target; + + @override + bool get isDisabled => uri == null; + + TestLinkInfo({ + required this.uri, + required this.target, + required this.builder, + }); +} diff --git a/packages/url_launcher/url_launcher_web/test/test_driver/url_launcher_web_integration.dart b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart similarity index 59% rename from packages/url_launcher/url_launcher_web/test/test_driver/url_launcher_web_integration.dart rename to packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart index 4d103443deb9..f7ea35667530 100644 --- a/packages/url_launcher/url_launcher_web/test/test_driver/url_launcher_web_integration.dart +++ b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart @@ -3,33 +3,35 @@ // found in the LICENSE file. import 'dart:html' as html; -import 'dart:js_util'; -import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:url_launcher_platform_interface/link.dart'; +import 'package:mockito/annotations.dart'; import 'package:url_launcher_web/url_launcher_web.dart'; -import 'package:url_launcher_web/src/link.dart'; import 'package:mockito/mockito.dart'; import 'package:integration_test/integration_test.dart'; -class _MockWindow extends Mock implements html.Window {} - -class _MockNavigator extends Mock implements html.Navigator {} +import 'url_launcher_web_test.mocks.dart'; +@GenerateMocks([html.Window, html.Navigator]) void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('UrlLauncherPlugin', () { - _MockWindow mockWindow; - _MockNavigator mockNavigator; + late MockWindow mockWindow; + late MockNavigator mockNavigator; - UrlLauncherPlugin plugin; + late UrlLauncherPlugin plugin; setUp(() { - mockWindow = _MockWindow(); - mockNavigator = _MockNavigator(); + mockWindow = MockWindow(); + mockNavigator = MockNavigator(); when(mockWindow.navigator).thenReturn(mockNavigator); + // Simulate that window.open does something. + when(mockWindow.open(any, any)).thenReturn(MockWindow()); + + when(mockNavigator.vendor).thenReturn('Google LLC'); + when(mockNavigator.appVersion).thenReturn('Mock version!'); + plugin = UrlLauncherPlugin(debugWindow: mockWindow); }); @@ -59,27 +61,10 @@ void main() { }); group('launch', () { - setUp(() { - // Simulate that window.open does something. - when(mockWindow.open('https://www.google.com', '')) - .thenReturn(_MockWindow()); - when(mockWindow.open('mailto:name@mydomain.com', '')) - .thenReturn(_MockWindow()); - when(mockWindow.open('tel:5551234567', '')).thenReturn(_MockWindow()); - when(mockWindow.open('sms:+19725551212?body=hello%20there', '')) - .thenReturn(_MockWindow()); - }); - testWidgets('launching a URL returns true', (WidgetTester _) async { expect( plugin.launch( 'https://www.google.com', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, ), completion(isTrue)); }); @@ -88,12 +73,6 @@ void main() { expect( plugin.launch( 'mailto:name@mydomain.com', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, ), completion(isTrue)); }); @@ -102,12 +81,6 @@ void main() { expect( plugin.launch( 'tel:5551234567', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, ), completion(isTrue)); }); @@ -116,12 +89,6 @@ void main() { expect( plugin.launch( 'sms:+19725551212?body=hello%20there', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, ), completion(isTrue)); }); @@ -232,88 +199,4 @@ void main() { }); }); }); - - group('link', () { - testWidgets('creates anchor with correct attributes', - (WidgetTester tester) async { - final Uri uri = Uri.parse('http://foobar/example?q=1'); - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: WebLinkDelegate(TestLinkInfo( - uri: uri, - target: LinkTarget.blank, - builder: (BuildContext context, FollowLink followLink) { - return Container(width: 100, height: 100); - }, - )), - )); - // Platform view creation happens asynchronously. - await tester.pumpAndSettle(); - - final html.Element anchor = _findSingleAnchor(); - expect(anchor.getAttribute('href'), uri.toString()); - expect(anchor.getAttribute('target'), '_blank'); - - final Uri uri2 = Uri.parse('http://foobar2/example?q=2'); - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: WebLinkDelegate(TestLinkInfo( - uri: uri2, - target: LinkTarget.self, - builder: (BuildContext context, FollowLink followLink) { - return Container(width: 100, height: 100); - }, - )), - )); - await tester.pumpAndSettle(); - - // Check that the same anchor has been updated. - expect(anchor.getAttribute('href'), uri2.toString()); - expect(anchor.getAttribute('target'), '_self'); - }); - }); -} - -html.Element _findSingleAnchor() { - final List foundAnchors = []; - for (final html.Element anchor in html.document.querySelectorAll('a')) { - if (hasProperty(anchor, linkViewIdProperty)) { - foundAnchors.add(anchor); - } - } - - // Search inside platform views with shadow roots as well. - for (final html.Element platformView - in html.document.querySelectorAll('flt-platform-view')) { - final html.ShadowRoot shadowRoot = platformView.shadowRoot; - if (shadowRoot != null) { - for (final html.Element anchor in shadowRoot.querySelectorAll('a')) { - if (hasProperty(anchor, linkViewIdProperty)) { - foundAnchors.add(anchor); - } - } - } - } - - return foundAnchors.single; -} - -class TestLinkInfo extends LinkInfo { - @override - final LinkWidgetBuilder builder; - - @override - final Uri uri; - - @override - final LinkTarget target; - - @override - bool get isDisabled => uri == null; - - TestLinkInfo({ - @required this.uri, - @required this.target, - @required this.builder, - }); } diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart new file mode 100644 index 000000000000..73d3bf355d67 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart @@ -0,0 +1,660 @@ +import 'dart:async' as _i4; +import 'dart:html' as _i2; +import 'dart:math' as _i5; +import 'dart:web_sql' as _i3; + +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: comment_references + +// ignore_for_file: unnecessary_parenthesis + +class _FakeDocument extends _i1.Fake implements _i2.Document {} + +class _FakeLocation extends _i1.Fake implements _i2.Location {} + +class _FakeConsole extends _i1.Fake implements _i2.Console {} + +class _FakeHistory extends _i1.Fake implements _i2.History {} + +class _FakeStorage extends _i1.Fake implements _i2.Storage {} + +class _FakeNavigator extends _i1.Fake implements _i2.Navigator {} + +class _FakePerformance extends _i1.Fake implements _i2.Performance {} + +class _FakeEvents extends _i1.Fake implements _i2.Events {} + +class _FakeType extends _i1.Fake implements Type {} + +class _FakeWindowBase extends _i1.Fake implements _i2.WindowBase {} + +class _FakeFileSystem extends _i1.Fake implements _i2.FileSystem {} + +class _FakeStylePropertyMapReadonly extends _i1.Fake + implements _i2.StylePropertyMapReadonly {} + +class _FakeMediaQueryList extends _i1.Fake implements _i2.MediaQueryList {} + +class _FakeEntry extends _i1.Fake implements _i2.Entry {} + +class _FakeSqlDatabase extends _i1.Fake implements _i3.SqlDatabase {} + +class _FakeGeolocation extends _i1.Fake implements _i2.Geolocation {} + +class _FakeMediaStream extends _i1.Fake implements _i2.MediaStream {} + +class _FakeRelatedApplication extends _i1.Fake + implements _i2.RelatedApplication {} + +/// A class which mocks [Window]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWindow extends _i1.Mock implements _i2.Window { + MockWindow() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future get animationFrame => + (super.noSuchMethod(Invocation.getter(#animationFrame), Future.value(0)) + as _i4.Future); + @override + _i2.Document get document => + (super.noSuchMethod(Invocation.getter(#document), _FakeDocument()) + as _i2.Document); + @override + _i2.Location get location => + (super.noSuchMethod(Invocation.getter(#location), _FakeLocation()) + as _i2.Location); + @override + set location(_i2.LocationBase? value) => + super.noSuchMethod(Invocation.setter(#location, value)); + @override + _i2.Console get console => + (super.noSuchMethod(Invocation.getter(#console), _FakeConsole()) + as _i2.Console); + @override + num get devicePixelRatio => + (super.noSuchMethod(Invocation.getter(#devicePixelRatio), 0) as num); + @override + _i2.History get history => + (super.noSuchMethod(Invocation.getter(#history), _FakeHistory()) + as _i2.History); + @override + _i2.Storage get localStorage => + (super.noSuchMethod(Invocation.getter(#localStorage), _FakeStorage()) + as _i2.Storage); + @override + _i2.Navigator get navigator => + (super.noSuchMethod(Invocation.getter(#navigator), _FakeNavigator()) + as _i2.Navigator); + @override + int get outerHeight => + (super.noSuchMethod(Invocation.getter(#outerHeight), 0) as int); + @override + int get outerWidth => + (super.noSuchMethod(Invocation.getter(#outerWidth), 0) as int); + @override + _i2.Performance get performance => + (super.noSuchMethod(Invocation.getter(#performance), _FakePerformance()) + as _i2.Performance); + @override + _i2.Storage get sessionStorage => + (super.noSuchMethod(Invocation.getter(#sessionStorage), _FakeStorage()) + as _i2.Storage); + @override + _i4.Stream<_i2.Event> get onContentLoaded => (super.noSuchMethod( + Invocation.getter(#onContentLoaded), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onAbort => (super + .noSuchMethod(Invocation.getter(#onAbort), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onBlur => + (super.noSuchMethod(Invocation.getter(#onBlur), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onCanPlay => (super.noSuchMethod( + Invocation.getter(#onCanPlay), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onCanPlayThrough => (super.noSuchMethod( + Invocation.getter(#onCanPlayThrough), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onChange => (super + .noSuchMethod(Invocation.getter(#onChange), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.MouseEvent> get onClick => (super.noSuchMethod( + Invocation.getter(#onClick), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onContextMenu => (super.noSuchMethod( + Invocation.getter(#onContextMenu), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.Event> get onDoubleClick => (super.noSuchMethod( + Invocation.getter(#onDoubleClick), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.DeviceMotionEvent> get onDeviceMotion => (super.noSuchMethod( + Invocation.getter(#onDeviceMotion), + Stream<_i2.DeviceMotionEvent>.empty()) + as _i4.Stream<_i2.DeviceMotionEvent>); + @override + _i4.Stream<_i2.DeviceOrientationEvent> get onDeviceOrientation => + (super.noSuchMethod(Invocation.getter(#onDeviceOrientation), + Stream<_i2.DeviceOrientationEvent>.empty()) + as _i4.Stream<_i2.DeviceOrientationEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDrag => (super.noSuchMethod( + Invocation.getter(#onDrag), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDragEnd => (super.noSuchMethod( + Invocation.getter(#onDragEnd), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDragEnter => (super.noSuchMethod( + Invocation.getter(#onDragEnter), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDragLeave => (super.noSuchMethod( + Invocation.getter(#onDragLeave), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDragOver => (super.noSuchMethod( + Invocation.getter(#onDragOver), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDragStart => (super.noSuchMethod( + Invocation.getter(#onDragStart), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDrop => (super.noSuchMethod( + Invocation.getter(#onDrop), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.Event> get onDurationChange => (super.noSuchMethod( + Invocation.getter(#onDurationChange), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onEmptied => (super.noSuchMethod( + Invocation.getter(#onEmptied), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onEnded => (super + .noSuchMethod(Invocation.getter(#onEnded), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onError => (super + .noSuchMethod(Invocation.getter(#onError), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onFocus => (super + .noSuchMethod(Invocation.getter(#onFocus), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onHashChange => (super.noSuchMethod( + Invocation.getter(#onHashChange), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onInput => (super + .noSuchMethod(Invocation.getter(#onInput), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onInvalid => (super.noSuchMethod( + Invocation.getter(#onInvalid), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.KeyboardEvent> get onKeyDown => (super.noSuchMethod( + Invocation.getter(#onKeyDown), Stream<_i2.KeyboardEvent>.empty()) + as _i4.Stream<_i2.KeyboardEvent>); + @override + _i4.Stream<_i2.KeyboardEvent> get onKeyPress => (super.noSuchMethod( + Invocation.getter(#onKeyPress), Stream<_i2.KeyboardEvent>.empty()) + as _i4.Stream<_i2.KeyboardEvent>); + @override + _i4.Stream<_i2.KeyboardEvent> get onKeyUp => (super.noSuchMethod( + Invocation.getter(#onKeyUp), Stream<_i2.KeyboardEvent>.empty()) + as _i4.Stream<_i2.KeyboardEvent>); + @override + _i4.Stream<_i2.Event> get onLoad => + (super.noSuchMethod(Invocation.getter(#onLoad), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onLoadedData => (super.noSuchMethod( + Invocation.getter(#onLoadedData), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onLoadedMetadata => (super.noSuchMethod( + Invocation.getter(#onLoadedMetadata), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onLoadStart => (super.noSuchMethod( + Invocation.getter(#onLoadStart), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.MessageEvent> get onMessage => (super.noSuchMethod( + Invocation.getter(#onMessage), Stream<_i2.MessageEvent>.empty()) + as _i4.Stream<_i2.MessageEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseDown => (super.noSuchMethod( + Invocation.getter(#onMouseDown), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseEnter => (super.noSuchMethod( + Invocation.getter(#onMouseEnter), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseLeave => (super.noSuchMethod( + Invocation.getter(#onMouseLeave), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseMove => (super.noSuchMethod( + Invocation.getter(#onMouseMove), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseOut => (super.noSuchMethod( + Invocation.getter(#onMouseOut), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseOver => (super.noSuchMethod( + Invocation.getter(#onMouseOver), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseUp => (super.noSuchMethod( + Invocation.getter(#onMouseUp), Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.WheelEvent> get onMouseWheel => (super.noSuchMethod( + Invocation.getter(#onMouseWheel), Stream<_i2.WheelEvent>.empty()) + as _i4.Stream<_i2.WheelEvent>); + @override + _i4.Stream<_i2.Event> get onOffline => (super.noSuchMethod( + Invocation.getter(#onOffline), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onOnline => (super + .noSuchMethod(Invocation.getter(#onOnline), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onPageHide => (super.noSuchMethod( + Invocation.getter(#onPageHide), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onPageShow => (super.noSuchMethod( + Invocation.getter(#onPageShow), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onPause => (super + .noSuchMethod(Invocation.getter(#onPause), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onPlay => + (super.noSuchMethod(Invocation.getter(#onPlay), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onPlaying => (super.noSuchMethod( + Invocation.getter(#onPlaying), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.PopStateEvent> get onPopState => (super.noSuchMethod( + Invocation.getter(#onPopState), Stream<_i2.PopStateEvent>.empty()) + as _i4.Stream<_i2.PopStateEvent>); + @override + _i4.Stream<_i2.Event> get onProgress => (super.noSuchMethod( + Invocation.getter(#onProgress), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onRateChange => (super.noSuchMethod( + Invocation.getter(#onRateChange), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onReset => (super + .noSuchMethod(Invocation.getter(#onReset), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onResize => (super + .noSuchMethod(Invocation.getter(#onResize), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onScroll => (super + .noSuchMethod(Invocation.getter(#onScroll), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onSearch => (super + .noSuchMethod(Invocation.getter(#onSearch), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onSeeked => (super + .noSuchMethod(Invocation.getter(#onSeeked), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onSeeking => (super.noSuchMethod( + Invocation.getter(#onSeeking), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onSelect => (super + .noSuchMethod(Invocation.getter(#onSelect), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onStalled => (super.noSuchMethod( + Invocation.getter(#onStalled), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.StorageEvent> get onStorage => (super.noSuchMethod( + Invocation.getter(#onStorage), Stream<_i2.StorageEvent>.empty()) + as _i4.Stream<_i2.StorageEvent>); + @override + _i4.Stream<_i2.Event> get onSubmit => (super + .noSuchMethod(Invocation.getter(#onSubmit), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onSuspend => (super.noSuchMethod( + Invocation.getter(#onSuspend), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onTimeUpdate => (super.noSuchMethod( + Invocation.getter(#onTimeUpdate), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.TouchEvent> get onTouchCancel => (super.noSuchMethod( + Invocation.getter(#onTouchCancel), Stream<_i2.TouchEvent>.empty()) + as _i4.Stream<_i2.TouchEvent>); + @override + _i4.Stream<_i2.TouchEvent> get onTouchEnd => (super.noSuchMethod( + Invocation.getter(#onTouchEnd), Stream<_i2.TouchEvent>.empty()) + as _i4.Stream<_i2.TouchEvent>); + @override + _i4.Stream<_i2.TouchEvent> get onTouchMove => (super.noSuchMethod( + Invocation.getter(#onTouchMove), Stream<_i2.TouchEvent>.empty()) + as _i4.Stream<_i2.TouchEvent>); + @override + _i4.Stream<_i2.TouchEvent> get onTouchStart => (super.noSuchMethod( + Invocation.getter(#onTouchStart), Stream<_i2.TouchEvent>.empty()) + as _i4.Stream<_i2.TouchEvent>); + @override + _i4.Stream<_i2.TransitionEvent> get onTransitionEnd => (super.noSuchMethod( + Invocation.getter(#onTransitionEnd), + Stream<_i2.TransitionEvent>.empty()) as _i4.Stream<_i2.TransitionEvent>); + @override + _i4.Stream<_i2.Event> get onUnload => (super + .noSuchMethod(Invocation.getter(#onUnload), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onVolumeChange => (super.noSuchMethod( + Invocation.getter(#onVolumeChange), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onWaiting => (super.noSuchMethod( + Invocation.getter(#onWaiting), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.AnimationEvent> get onAnimationEnd => (super.noSuchMethod( + Invocation.getter(#onAnimationEnd), + Stream<_i2.AnimationEvent>.empty()) as _i4.Stream<_i2.AnimationEvent>); + @override + _i4.Stream<_i2.AnimationEvent> get onAnimationIteration => + (super.noSuchMethod(Invocation.getter(#onAnimationIteration), + Stream<_i2.AnimationEvent>.empty()) + as _i4.Stream<_i2.AnimationEvent>); + @override + _i4.Stream<_i2.AnimationEvent> get onAnimationStart => (super.noSuchMethod( + Invocation.getter(#onAnimationStart), + Stream<_i2.AnimationEvent>.empty()) as _i4.Stream<_i2.AnimationEvent>); + @override + _i4.Stream<_i2.Event> get onBeforeUnload => (super.noSuchMethod( + Invocation.getter(#onBeforeUnload), Stream<_i2.Event>.empty()) + as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.WheelEvent> get onWheel => (super.noSuchMethod( + Invocation.getter(#onWheel), Stream<_i2.WheelEvent>.empty()) + as _i4.Stream<_i2.WheelEvent>); + @override + int get pageXOffset => + (super.noSuchMethod(Invocation.getter(#pageXOffset), 0) as int); + @override + int get pageYOffset => + (super.noSuchMethod(Invocation.getter(#pageYOffset), 0) as int); + @override + int get scrollX => + (super.noSuchMethod(Invocation.getter(#scrollX), 0) as int); + @override + int get scrollY => + (super.noSuchMethod(Invocation.getter(#scrollY), 0) as int); + @override + _i2.Events get on => + (super.noSuchMethod(Invocation.getter(#on), _FakeEvents()) as _i2.Events); + @override + int get hashCode => + (super.noSuchMethod(Invocation.getter(#hashCode), 0) as int); + @override + Type get runtimeType => + (super.noSuchMethod(Invocation.getter(#runtimeType), _FakeType()) + as Type); + @override + _i2.WindowBase open(String? url, String? name, [String? options]) => + (super.noSuchMethod( + Invocation.method(#open, [url, name, options]), _FakeWindowBase()) + as _i2.WindowBase); + @override + int requestAnimationFrame(_i2.FrameRequestCallback? callback) => + (super.noSuchMethod( + Invocation.method(#requestAnimationFrame, [callback]), 0) as int); + @override + void cancelAnimationFrame(int? id) => + super.noSuchMethod(Invocation.method(#cancelAnimationFrame, [id])); + @override + _i4.Future<_i2.FileSystem> requestFileSystem(int? size, {bool? persistent}) => + (super.noSuchMethod( + Invocation.method( + #requestFileSystem, [size], {#persistent: persistent}), + Future.value(_FakeFileSystem())) as _i4.Future<_i2.FileSystem>); + @override + void cancelIdleCallback(int? handle) => + super.noSuchMethod(Invocation.method(#cancelIdleCallback, [handle])); + @override + bool confirm([String? message]) => + (super.noSuchMethod(Invocation.method(#confirm, [message]), false) + as bool); + @override + _i4.Future fetch(dynamic input, [Map? init]) => + (super.noSuchMethod( + Invocation.method(#fetch, [input, init]), Future.value(null)) + as _i4.Future); + @override + bool find(String? string, bool? caseSensitive, bool? backwards, bool? wrap, + bool? wholeWord, bool? searchInFrames, bool? showDialog) => + (super.noSuchMethod( + Invocation.method(#find, [ + string, + caseSensitive, + backwards, + wrap, + wholeWord, + searchInFrames, + showDialog + ]), + false) as bool); + @override + _i2.StylePropertyMapReadonly getComputedStyleMap( + _i2.Element? element, String? pseudoElement) => + (super.noSuchMethod( + Invocation.method(#getComputedStyleMap, [element, pseudoElement]), + _FakeStylePropertyMapReadonly()) as _i2.StylePropertyMapReadonly); + @override + List<_i2.CssRule> getMatchedCssRules( + _i2.Element? element, String? pseudoElement) => + (super.noSuchMethod( + Invocation.method(#getMatchedCssRules, [element, pseudoElement]), + <_i2.CssRule>[]) as List<_i2.CssRule>); + @override + _i2.MediaQueryList matchMedia(String? query) => (super.noSuchMethod( + Invocation.method(#matchMedia, [query]), _FakeMediaQueryList()) + as _i2.MediaQueryList); + @override + void moveBy(int? x, int? y) => + super.noSuchMethod(Invocation.method(#moveBy, [x, y])); + @override + void postMessage(dynamic message, String? targetOrigin, + [List? transfer]) => + super.noSuchMethod( + Invocation.method(#postMessage, [message, targetOrigin, transfer])); + @override + int requestIdleCallback(_i2.IdleRequestCallback? callback, + [Map? options]) => + (super.noSuchMethod( + Invocation.method(#requestIdleCallback, [callback, options]), 0) + as int); + @override + void resizeBy(int? x, int? y) => + super.noSuchMethod(Invocation.method(#resizeBy, [x, y])); + @override + void resizeTo(int? x, int? y) => + super.noSuchMethod(Invocation.method(#resizeTo, [x, y])); + @override + _i4.Future<_i2.Entry> resolveLocalFileSystemUrl(String? url) => + (super.noSuchMethod(Invocation.method(#resolveLocalFileSystemUrl, [url]), + Future.value(_FakeEntry())) as _i4.Future<_i2.Entry>); + @override + String atob(String? atob) => + (super.noSuchMethod(Invocation.method(#atob, [atob]), '') as String); + @override + String btoa(String? btoa) => + (super.noSuchMethod(Invocation.method(#btoa, [btoa]), '') as String); + @override + void moveTo(_i5.Point? p) => + super.noSuchMethod(Invocation.method(#moveTo, [p])); + @override + _i3.SqlDatabase openDatabase(String? name, String? version, + String? displayName, int? estimatedSize, + [_i2.DatabaseCallback? creationCallback]) => + (super.noSuchMethod( + Invocation.method(#openDatabase, + [name, version, displayName, estimatedSize, creationCallback]), + _FakeSqlDatabase()) as _i3.SqlDatabase); + @override + void addEventListener(String? type, _i2.EventListener? listener, + [bool? useCapture]) => + super.noSuchMethod( + Invocation.method(#addEventListener, [type, listener, useCapture])); + @override + void removeEventListener(String? type, _i2.EventListener? listener, + [bool? useCapture]) => + super.noSuchMethod(Invocation.method( + #removeEventListener, [type, listener, useCapture])); + @override + bool dispatchEvent(_i2.Event? event) => + (super.noSuchMethod(Invocation.method(#dispatchEvent, [event]), false) + as bool); + @override + bool operator ==(Object? other) => + (super.noSuchMethod(Invocation.method(#==, [other]), false) as bool); + @override + String toString() => + (super.noSuchMethod(Invocation.method(#toString, []), '') as String); +} + +/// A class which mocks [Navigator]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNavigator extends _i1.Mock implements _i2.Navigator { + MockNavigator() { + _i1.throwOnMissingStub(this); + } + + @override + String get language => + (super.noSuchMethod(Invocation.getter(#language), '') as String); + @override + _i2.Geolocation get geolocation => + (super.noSuchMethod(Invocation.getter(#geolocation), _FakeGeolocation()) + as _i2.Geolocation); + @override + String get vendor => + (super.noSuchMethod(Invocation.getter(#vendor), '') as String); + @override + String get vendorSub => + (super.noSuchMethod(Invocation.getter(#vendorSub), '') as String); + @override + String get appCodeName => + (super.noSuchMethod(Invocation.getter(#appCodeName), '') as String); + @override + String get appName => + (super.noSuchMethod(Invocation.getter(#appName), '') as String); + @override + String get appVersion => + (super.noSuchMethod(Invocation.getter(#appVersion), '') as String); + @override + String get product => + (super.noSuchMethod(Invocation.getter(#product), '') as String); + @override + String get userAgent => + (super.noSuchMethod(Invocation.getter(#userAgent), '') as String); + @override + int get hashCode => + (super.noSuchMethod(Invocation.getter(#hashCode), 0) as int); + @override + Type get runtimeType => + (super.noSuchMethod(Invocation.getter(#runtimeType), _FakeType()) + as Type); + @override + List<_i2.Gamepad?> getGamepads() => + (super.noSuchMethod(Invocation.method(#getGamepads, []), <_i2.Gamepad?>[]) + as List<_i2.Gamepad?>); + @override + _i4.Future<_i2.MediaStream> getUserMedia({dynamic audio, dynamic video}) => + (super.noSuchMethod( + Invocation.method(#getUserMedia, [], {#audio: audio, #video: video}), + Future.value(_FakeMediaStream())) as _i4.Future<_i2.MediaStream>); + @override + _i4.Future getBattery() => (super + .noSuchMethod(Invocation.method(#getBattery, []), Future.value(null)) + as _i4.Future); + @override + _i4.Future<_i2.RelatedApplication> getInstalledRelatedApps() => + (super.noSuchMethod(Invocation.method(#getInstalledRelatedApps, []), + Future.value(_FakeRelatedApplication())) + as _i4.Future<_i2.RelatedApplication>); + @override + _i4.Future getVRDisplays() => (super.noSuchMethod( + Invocation.method(#getVRDisplays, []), Future.value(null)) + as _i4.Future); + @override + void registerProtocolHandler(String? scheme, String? url, String? title) => + super.noSuchMethod( + Invocation.method(#registerProtocolHandler, [scheme, url, title])); + @override + _i4.Future requestKeyboardLock([List? keyCodes]) => + (super.noSuchMethod(Invocation.method(#requestKeyboardLock, [keyCodes]), + Future.value(null)) as _i4.Future); + @override + _i4.Future requestMidiAccess([Map? options]) => + (super.noSuchMethod(Invocation.method(#requestMidiAccess, [options]), + Future.value(null)) as _i4.Future); + @override + _i4.Future requestMediaKeySystemAccess(String? keySystem, + List>? supportedConfigurations) => + (super.noSuchMethod( + Invocation.method(#requestMediaKeySystemAccess, + [keySystem, supportedConfigurations]), + Future.value(null)) as _i4.Future); + @override + bool sendBeacon(String? url, Object? data) => + (super.noSuchMethod(Invocation.method(#sendBeacon, [url, data]), false) + as bool); + @override + _i4.Future share([Map? data]) => + (super.noSuchMethod(Invocation.method(#share, [data]), Future.value(null)) + as _i4.Future); + @override + bool operator ==(Object? other) => + (super.noSuchMethod(Invocation.method(#==, [other]), false) as bool); + @override + String toString() => + (super.noSuchMethod(Invocation.method(#toString, []), '') as String); +} diff --git a/packages/url_launcher/url_launcher_web/example/lib/main.dart b/packages/url_launcher/url_launcher_web/example/lib/main.dart new file mode 100644 index 000000000000..e1a38dcdcd46 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/lib/main.dart @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +void main() { + runApp(MyApp()); +} + +/// App for testing +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Text('Testing... Look at the console output for results!'), + ); + } +} diff --git a/packages/url_launcher/url_launcher_web/test/pubspec.yaml b/packages/url_launcher/url_launcher_web/example/pubspec.yaml similarity index 60% rename from packages/url_launcher/url_launcher_web/test/pubspec.yaml rename to packages/url_launcher/url_launcher_web/example/pubspec.yaml index e755dff85004..51748610e971 100644 --- a/packages/url_launcher/url_launcher_web/test/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/example/pubspec.yaml @@ -1,22 +1,22 @@ name: regular_integration_tests publish_to: none -environment: - sdk: ">=2.2.2 <3.0.0" - dependencies: flutter: sdk: flutter dev_dependencies: + build_runner: ^1.10.0 + mockito: ^5.0.0-nullsafety.5 + url_launcher_web: + path: ../ flutter_driver: sdk: flutter flutter_test: sdk: flutter - http: ^0.12.2 - mockito: ^4.1.1 - url_launcher_web: - path: ../ integration_test: - path: ../../../integration_test + sdk: flutter +environment: + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.26.0-0" # For integration_test from sdk diff --git a/packages/url_launcher/url_launcher_web/example/run_test.sh b/packages/url_launcher/url_launcher_web/example/run_test.sh new file mode 100755 index 000000000000..b243f2938b1f --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/run_test.sh @@ -0,0 +1,23 @@ +#!/usr/bin/bash +if pgrep -lf chromedriver > /dev/null; then + echo "chromedriver is running." + + flutter pub get + + echo "(Re)generating mocks." + flutter pub run build_runner build --delete-conflicting-outputs + + if [ $# -eq 0 ]; then + echo "No target specified, running all tests..." + find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test_driver.dart --target='{}' + else + echo "Running test target: $1..." + set -x + flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test_driver.dart --target=$1 + fi + + else + echo "chromedriver is not running." + echo "Please, check the README.md for instructions on how to use run_test.sh" +fi + diff --git a/packages/url_launcher/url_launcher_web/test/test_driver/url_launcher_web_integration_test.dart b/packages/url_launcher/url_launcher_web/example/test_driver/integration_test_driver.dart similarity index 100% rename from packages/url_launcher/url_launcher_web/test/test_driver/url_launcher_web_integration_test.dart rename to packages/url_launcher/url_launcher_web/example/test_driver/integration_test_driver.dart diff --git a/packages/url_launcher/url_launcher_web/example/web/index.html b/packages/url_launcher/url_launcher_web/example/web/index.html new file mode 100644 index 000000000000..dc8d0cfe0428 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/web/index.html @@ -0,0 +1,12 @@ + + + + + Browser Tests + + + + + diff --git a/packages/url_launcher/url_launcher_web/ios/url_launcher_web.podspec b/packages/url_launcher/url_launcher_web/ios/url_launcher_web.podspec deleted file mode 100644 index 161156ef020d..000000000000 --- a/packages/url_launcher/url_launcher_web/ios/url_launcher_web.podspec +++ /dev/null @@ -1,20 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'url_launcher_web' - s.version = '0.0.1' - s.summary = 'No-op implementation of url_launcher_web web plugin to avoid build issues on iOS' - s.description = <<-DESC -temp fake url_launcher_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end diff --git a/packages/url_launcher/url_launcher_web/lib/src/link.dart b/packages/url_launcher/url_launcher_web/lib/src/link.dart index e8a6d68348bb..42c711b7d818 100644 --- a/packages/url_launcher/url_launcher_web/lib/src/link.dart +++ b/packages/url_launcher/url_launcher_web/lib/src/link.dart @@ -45,16 +45,16 @@ class WebLinkDelegate extends StatefulWidget { /// For external URIs, it lets the browser do its thing. For app route names, it /// pushes the route name to the framework. class WebLinkDelegateState extends State { - LinkViewController _controller; + late LinkViewController _controller; @override void didUpdateWidget(WebLinkDelegate oldWidget) { super.didUpdateWidget(oldWidget); if (widget.link.uri != oldWidget.link.uri) { - _controller?.setUri(widget.link.uri); + _controller.setUri(widget.link.uri); } if (widget.link.target != oldWidget.link.target) { - _controller?.setTarget(widget.link.target); + _controller.setTarget(widget.link.target); } } @@ -66,6 +66,7 @@ class WebLinkDelegateState extends State { @override Widget build(BuildContext context) { return Stack( + fit: StackFit.passthrough, children: [ widget.link.builder( context, @@ -125,15 +126,15 @@ class LinkViewController extends PlatformViewController { static Map _instances = {}; static html.Element _viewFactory(int viewId) { - return _instances[viewId]?._element; + return _instances[viewId]!._element; } - static int _hitTestedViewId; + static int? _hitTestedViewId; - static StreamSubscription _clickSubscription; + static late StreamSubscription _clickSubscription; static void _onGlobalClick(html.MouseEvent event) { - final int viewId = getViewIdFromTarget(event); + final int? viewId = getViewIdFromTarget(event); _instances[viewId]?._onDomClick(event); // After the DOM click event has been received, clean up the hit test state // so we can start fresh on the next click. @@ -160,7 +161,7 @@ class LinkViewController extends PlatformViewController { /// The context of the [Link] widget that created this controller. final BuildContext context; - html.Element _element; + late html.Element _element; bool get _isInitialized => _element != null; Future _initialize() async { @@ -192,7 +193,7 @@ class LinkViewController extends PlatformViewController { return; } - if (_uri.hasScheme) { + if (_uri != null && _uri!.hasScheme) { // External links will be handled by the browser, so we don't have to do // anything. return; @@ -206,10 +207,12 @@ class LinkViewController extends PlatformViewController { pushRouteNameToFramework(context, routeName); } - Uri _uri; + Uri? _uri; /// Set the [Uri] value for this link. - void setUri(Uri uri) { + /// + /// When Uri is null, the `href` attribute of the link is removed. + void setUri(Uri? uri) { assert(_isInitialized); _uri = uri; if (uri == null) { @@ -263,8 +266,8 @@ class LinkViewController extends PlatformViewController { } /// Finds the view id of the DOM element targeted by the [event]. -int getViewIdFromTarget(html.Event event) { - final html.Element linkElement = getLinkElementFromTarget(event); +int? getViewIdFromTarget(html.Event event) { + final html.Element? linkElement = getLinkElementFromTarget(event); if (linkElement != null) { return getProperty(linkElement, linkViewIdProperty); } @@ -274,15 +277,17 @@ int getViewIdFromTarget(html.Event event) { /// Finds the targeted DOM element by the [event]. /// /// It handles the case where the target element is inside a shadow DOM too. -html.Element getLinkElementFromTarget(html.Event event) { - final html.Element target = event.target; - if (isLinkElement(target)) { - return target; - } - if (target.shadowRoot != null) { - final html.Element child = target.shadowRoot.lastChild; - if (isLinkElement(child)) { - return child; +html.Element? getLinkElementFromTarget(html.Event event) { + final html.EventTarget? target = event.target; + if (target != null && target is html.Element) { + if (isLinkElement(target)) { + return target; + } + if (target.shadowRoot != null) { + final html.Node? child = target.shadowRoot!.lastChild; + if (child != null && child is html.Element && isLinkElement(child)) { + return child; + } } } return null; @@ -290,6 +295,8 @@ html.Element getLinkElementFromTarget(html.Event event) { /// Checks if the given [element] is a link that was created by /// [LinkViewController]. -bool isLinkElement(html.Element element) { - return element.tagName == 'A' && hasProperty(element, linkViewIdProperty); +bool isLinkElement(html.Element? element) { + return element != null && + element.tagName == 'A' && + hasProperty(element, linkViewIdProperty); } diff --git a/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart index 969cfbaa2dfd..e4d2116445cf 100644 --- a/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart +++ b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart @@ -19,7 +19,7 @@ const _safariTargetTopSchemes = { 'tel', 'sms', }; -String _getUrlScheme(String url) => Uri.tryParse(url)?.scheme; +String? _getUrlScheme(String url) => Uri.tryParse(url)?.scheme; bool _isSafariTargetTopScheme(String url) => _safariTargetTopSchemes.contains(_getUrlScheme(url)); @@ -38,7 +38,7 @@ class UrlLauncherPlugin extends UrlLauncherPlatform { }.union(_safariTargetTopSchemes); /// A constructor that allows tests to override the window object used by the plugin. - UrlLauncherPlugin({@visibleForTesting html.Window debugWindow}) + UrlLauncherPlugin({@visibleForTesting html.Window? debugWindow}) : _window = debugWindow ?? html.window { _isSafari = navigatorIsSafari(_window.navigator); } @@ -58,7 +58,7 @@ class UrlLauncherPlugin extends UrlLauncherPlatform { /// /// Returns the newly created window. @visibleForTesting - html.WindowBase openNewWindow(String url, {String webOnlyWindowName}) { + html.WindowBase openNewWindow(String url, {String? webOnlyWindowName}) { // We need to open mailto, tel and sms urls on the _top window context on safari browsers. // See https://github.com/flutter/flutter/issues/51461 for reference. final target = webOnlyWindowName ?? @@ -74,13 +74,13 @@ class UrlLauncherPlugin 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, + bool useSafariVC = false, + bool useWebView = false, + bool enableJavaScript = false, + bool enableDomStorage = false, + bool universalLinksOnly = false, + Map headers = const {}, + String? webOnlyWindowName, }) { return Future.value( openNewWindow(url, webOnlyWindowName: webOnlyWindowName) != null); diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index b40a8ea236cc..371a40e16f7b 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -1,10 +1,7 @@ name: url_launcher_web description: Web platform implementation of url_launcher homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_web -# 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: 2.0.0 flutter: plugin: @@ -14,22 +11,18 @@ flutter: fileName: url_launcher_web.dart dependencies: - url_launcher_platform_interface: ^1.0.9 + url_launcher_platform_interface: ^2.0.0 + meta: ^1.3.0 # null safe flutter: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 dev_dependencies: + pedantic: ^1.10.0 # null safe flutter_test: sdk: flutter - url_launcher: ^5.2.5 - pedantic: ^1.8.0 - mockito: ^4.1.1 - integration_test: - path: ../../integration_test environment: - sdk: ">=2.2.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/url_launcher/url_launcher_web/test/README.md b/packages/url_launcher/url_launcher_web/test/README.md index 7c48d024ba57..7c5b4ad682ba 100644 --- a/packages/url_launcher/url_launcher_web/test/README.md +++ b/packages/url_launcher/url_launcher_web/test/README.md @@ -1,17 +1,5 @@ -# Running browser_tests +## test -Make sure you have updated to the latest Flutter master. +This package uses integration tests for testing. -1. Check what version of Chrome is running on the machine you're running tests on. - -2. Download and install driver for that version from here: - * - -3. Start the driver using `chromedriver --port=4444` - -4. Change into the `test` directory of your clone. - -5. Run tests: `flutter drive -d web-server --browser-name=chrome --target=test_driver/TEST_NAME_integration.dart`, or (in Linux): - - * Single: `./run_test test_driver/TEST_NAME_integration.dart` - * All: `./run_test` +See `example/README.md` for more info. diff --git a/packages/url_launcher/url_launcher_web/test/tests_exist_elsewhere_test.dart b/packages/url_launcher/url_launcher_web/test/tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..334f52186d9d --- /dev/null +++ b/packages/url_launcher/url_launcher_web/test/tests_exist_elsewhere_test.dart @@ -0,0 +1,10 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Tell the user where to find the real tests', () { + print('---'); + print('This package uses integration_test for its tests.'); + print('See `example/README.md` for more info.'); + print('---'); + }); +} diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index 3bf040f964e6..e906254eef44 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,3 +1,17 @@ +## 2.0.0 + +* Migrate to null-safety. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Set `implementation` in pubspec.yaml + +## 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/README.md b/packages/url_launcher/url_launcher_windows/README.md index fb5ad6700d26..4cebb7ed91fb 100644 --- a/packages/url_launcher/url_launcher_windows/README.md +++ b/packages/url_launcher/url_launcher_windows/README.md @@ -2,13 +2,6 @@ The Windows implementation of [`url_launcher`][1]. -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. If you use -url_launcher_windows directly, rather than as an implementation detail -of `url_launcher`, please use `url_launcher_windows: '>=0.0.y+x <2.0.0'` -as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package 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..e1008fddd4e1 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 @@ -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:url_launcher_platform_interface/url_launcher_platform_interface.dart'; @@ -9,7 +11,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..f49e9fa290c5 100644 --- a/packages/url_launcher/url_launcher_windows/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_windows/example/lib/main.dart @@ -26,7 +26,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 @@ -34,7 +34,7 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - Future _launched; + Future? _launched; Future _launchInBrowser(String url) async { if (await UrlLauncherPlatform.instance.canLaunch(url)) { @@ -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/example/pubspec.yaml b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml index 44a5424168c9..8a273ba65020 100644 --- a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml @@ -4,8 +4,13 @@ description: Demonstrates the Windows implementation of the url_launcher plugin. dependencies: flutter: sdk: flutter - url_launcher_platform_interface: any + url_launcher_platform_interface: ^2.0.0 url_launcher_windows: + # When depending on this package from a real application you should use: + # url_launcher_windows: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -13,11 +18,11 @@ dev_dependencies: path: ../../../integration_test flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" diff --git a/packages/url_launcher/url_launcher_windows/example/test_driver/integration_test.dart b/packages/url_launcher/url_launcher_windows/example/test_driver/integration_test.dart index 7a2c21338786..a8a56aa90f6a 100644 --- a/packages/url_launcher/url_launcher_windows/example/test_driver/integration_test.dart +++ b/packages/url_launcher/url_launcher_windows/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/url_launcher/url_launcher_windows/ios/url_launcher_windows.podspec b/packages/url_launcher/url_launcher_windows/ios/url_launcher_windows.podspec deleted file mode 100644 index 1c700d49f4b5..000000000000 --- a/packages/url_launcher/url_launcher_windows/ios/url_launcher_windows.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# Run `pod lib lint url_launcher_windows.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'url_launcher_windows' - s.version = '0.0.1' - s.summary = 'url_launcher_windows iOS stub' - s.description = <<-DESC - No-op implementation of the windows url_launcher plugin to avoid build issues on iOS - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_windows' } - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index dd7423d017f8..e5b611f86af0 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -1,20 +1,18 @@ name: url_launcher_windows 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: 2.0.0 homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_windows flutter: plugin: + implements: url_launcher platforms: windows: pluginClass: UrlLauncherPlugin environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 8f7f7516ae3a..bc9d189635d3 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,22 @@ +## 2.0.1 + +* Refactor `FLTCMTimeToMillis` to support indefinite streams - fixes [flutter/flutter#48670](Fixes https://github.com/flutter/flutter/issues/48670). + +## 2.0.0 + +* Migrate to null safety. +* Fix an issue where `isBuffering` was not updating on Android. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Fix `VideoPlayerValue toString()` test. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Migrate from deprecated `defaultBinaryMessenger`. +* Fix an issue where a crash can occur after a closing a video player view on iOS. +* Setting the `mixWithOthers` `VideoPlayerOptions` in web now is silently ignored instead of throwing an exception. + +## 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/CONTRIBUTING.md b/packages/video_player/video_player/CONTRIBUTING.md new file mode 100644 index 000000000000..32c9d1b791d1 --- /dev/null +++ b/packages/video_player/video_player/CONTRIBUTING.md @@ -0,0 +1,82 @@ +## Updating pigeon-generated files + +If you update files in the pigeons/ directory, run the following +command in this directory (ignore the errors you get about +dependencies in the examples directory): + +```bash +flutter pub upgrade +flutter pub run pigeon --dart_null_safety --input pigeons/messages.dart +# git commit your changes so that your working environment is clean +(cd ../../../; ./script/incremental_build.sh format --travis --clang-format=clang-format-7) +``` + +If you update pigeon itself and want to test the changes here, +temporarily update the pubspec.yaml by adding the following to the +`dependency_overrides` section, assuming you have checked out the +`flutter/packages` repo in a sibling directory to the `plugins` repo: + +```yaml + pigeon: + path: + ../../../../packages/packages/pigeon/ +``` + +Then, run the commands above. When you run `pub get` it should warn +you that you're using an override. If you do this, you will need to +publish pigeon before you can land the updates to this package, since +the CI tests run the analysis using latest published version of +pigeon, not your version or the version on master. + +In either case, the configuration will be obtained automatically from +the `pigeons/messages.dart` file (see `configurePigeon` at the bottom +of that file). + +While contributing, you may also want to set the following dependency +overrides: + +```yaml +dependency_overrides: + video_player_platform_interface: + path: + ../video_player_platform_interface + video_player_web: + path: + ../video_player_web +``` + +## Publishing plugin updates that span multiple plugin packages + +If your change affects both the interface package and the +implementation packages, then you will need to publish a version of +the plugin in between landing the interface changes and the +implementation changes, since the implementations depend on the +interface via pub. + +To do this, follow these steps: + +1. Create a PR that has all the changes, and update the +`pubspec.yaml`s to have path-based dependency overrides as described +in the "Updating pigeon-generated files" section above. + +2. Upload that PR and get it reviewed and into a state where the only +test failure is the one complaining that you can't publish a package +that has dependency overrides. + +3. Create a PR that's a subset of the one in the previous step that +only includes the interface changes, with no dependency overrides, and +submit that. + +4. Once you have had that reviewed and landed, publish the interface +parts of the plugin to pub. + +5. Now, update the original full PR to not use dependency overrides +but to instead refer to the new version of the plugin, and sync it to +master (so that the interface changes are gone from the PR). Submit +that PR. + +6. Once you have had _that_ PR reviewed and landed, publish the +implementation parts of the plugin to pub. + +You may need to publish each implementation package independently of +the main package also, depending on exactly what your change entails. diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index 5eff770b87a2..7e36008cbbc3 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 @@ -48,6 +48,8 @@ This plugin compiles for the web platform since version `0.10.5`, in recent enou Different web browsers may have different video-playback capabilities (supported formats, autoplay...). Check [package:video_player_web](https://pub.dev/packages/video_player_web) for more web-specific information. +The `VideoPlayerOptions.mixWithOthers` option can't be implemented in web, at least at the moment. If you use this option in web it will be silently ignored. + ## Supported Formats - On iOS, the backing player is [AVPlayer](https://developer.apple.com/documentation/avfoundation/avplayer). @@ -77,7 +79,7 @@ class _VideoAppState extends State { void initState() { super.initState(); _controller = VideoPlayerController.network( - 'http://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4') + 'https://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4') ..initialize().then((_) { // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. setState(() {}); 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..053e3faa9694 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.19), 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 ab0c43d78ffa..49a5a184bd2f 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(); Map event = new HashMap<>(); event.put("event", "bufferingStart"); @@ -191,10 +202,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..921ba43576d3 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,82 @@ 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( + 'live stream duration != 0', + (WidgetTester tester) async { + VideoPlayerController networkController = VideoPlayerController.network( + 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', + ); + await networkController.initialize(); + + expect(_controller.value.isInitialized, true); + // Live streams should have either a positive duration or C.TIME_UNSET if the duration is unknown + // See https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#getDuration-- + expect(networkController.value.duration, + (Duration duration) => duration != Duration.zero); + }, + skip: (kIsWeb), + ); + 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 +129,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 +178,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..4bfb3e5fefad 100644 --- a/packages/video_player/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -7,6 +7,11 @@ dependencies: flutter: sdk: flutter video_player: + # When depending on this package from a real application you should use: + # video_player: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -16,7 +21,8 @@ dev_dependencies: sdk: flutter integration_test: path: ../../../integration_test - pedantic: ^1.8.0 + test: any + pedantic: ^1.10.0 flutter: uses-material-design: true @@ -26,5 +32,5 @@ flutter: - assets/bumble_bee_captions.srt environment: - sdk: ">=2.8.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.12.13+hotfix.5" 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/FLTVideoPlayerPlugin.m b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m index e6a4f6ccb0b7..0fd36599195f 100644 --- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m @@ -11,11 +11,6 @@ #error Code Requires ARC. #endif -int64_t FLTCMTimeToMillis(CMTime time) { - if (time.timescale == 0) return 0; - return time.value * 1000 / time.timescale; -} - @interface FLTFrameUpdater : NSObject @property(nonatomic) int64_t textureId; @property(nonatomic, weak, readonly) NSObject* registry; @@ -105,6 +100,16 @@ - (void)itemDidPlayToEndTime:(NSNotification*)notification { } } +const int64_t TIME_UNSET = -9223372036854775807; + +static inline int64_t FLTCMTimeToMillis(CMTime time) { + // When CMTIME_IS_INDEFINITE return a value that matches TIME_UNSET from ExoPlayer2 on Android. + // Fixes https://github.com/flutter/flutter/issues/48670 + if (CMTIME_IS_INDEFINITE(time)) return TIME_UNSET; + if (time.timescale == 0) return 0; + return time.value * 1000 / time.timescale; +} + static inline CGFloat radiansToDegrees(CGFloat radians) { // Input range [-pi, pi] or [-180, 180] CGFloat degrees = GLKMathRadiansToDegrees((float)radians); @@ -392,7 +397,7 @@ - (CVPixelBufferRef)copyPixelBuffer { } } -- (void)onTextureUnregistered { +- (void)onTextureUnregistered:(NSObject*)texture { dispatch_async(dispatch_get_main_queue(), ^{ [self dispose]; }); diff --git a/packages/video_player/video_player/ios/Classes/messages.h b/packages/video_player/video_player/ios/Classes/messages.h index 3c68b3dd24d4..80137c9d61f5 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.19), 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..3f787fcdf92d 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.19), 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..ebef9e526b6a 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 { @@ -52,6 +54,7 @@ abstract class VideoPlayerApi { void configurePigeon(PigeonOptions opts) { opts.dartOut = '../video_player_platform_interface/lib/messages.dart'; + opts.dartTestOut = '../video_player_platform_interface/lib/test.dart'; opts.objcHeaderOut = 'ios/Classes/messages.h'; opts.objcSourceOut = 'ios/Classes/messages.m'; opts.objcOptions.prefix = 'FLT'; diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 69be8b24100b..3c6a34c7f78c 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -1,7 +1,7 @@ name: video_player description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. -version: 1.0.1 +version: 2.0.1 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player flutter: @@ -16,25 +16,26 @@ flutter: default_package: video_player_web dependencies: - meta: ^1.0.5 - video_player_platform_interface: ^2.2.0 + meta: ^1.3.0 + video_player_platform_interface: ^4.0.0 # 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). + # validation, so we set a ^ constraint. The exact value doesn't matter since + # the constraints on the interface pins it. + # 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 flutter: sdk: flutter - -dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 - pigeon: 0.1.7 + +dev_dependencies: + pedantic: ^1.10.0 + pigeon: ^0.1.21 environment: - sdk: ">=2.8.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0-259.9.beta <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..582012097b71 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'; @@ -14,11 +12,12 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player/video_player.dart'; import 'package:video_player_platform_interface/messages.dart'; +import 'package:video_player_platform_interface/test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; class FakeController extends ValueNotifier implements VideoPlayerController { - FakeController() : super(VideoPlayerValue(duration: null)); + FakeController() : super(VideoPlayerValue(duration: Duration.zero)); @override Future dispose() async { @@ -26,7 +25,7 @@ class FakeController extends ValueNotifier } @override - int textureId; + int textureId = VideoPlayerController.kUninitializedTextureId; @override String get dataSource => ''; @@ -35,7 +34,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 +61,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 +79,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 +102,7 @@ void main() { controller.textureId = 123; controller.value = controller.value.copyWith( duration: const Duration(milliseconds: 100), + isInitialized: true, ); await tester.pump(); @@ -133,8 +135,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 +151,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 +175,7 @@ void main() { }); group('VideoPlayerController', () { - FakeVideoPlayerPlatform fakeVideoPlayerPlatform; + late FakeVideoPlayerPlatform fakeVideoPlayerPlatform; setUp(() { fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); @@ -221,7 +223,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 +247,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 +393,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 +422,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 +440,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 +497,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 +507,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 +517,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 +527,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 +537,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 +555,7 @@ void main() { position: position, caption: caption, buffered: buffered, + isInitialized: isInitialized, isPlaying: isPlaying, isLooping: isLooping, isBuffering: isBuffering, @@ -566,8 +568,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 +589,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 +606,7 @@ void main() { test('height = 0 -> 1.0', () { final value = VideoPlayerValue( + isInitialized: true, size: Size(640, 0), duration: Duration(seconds: 1), ); @@ -610,6 +615,7 @@ void main() { test('width = 0 -> 1.0', () { final value = VideoPlayerValue( + isInitialized: true, size: Size(0, 480), duration: Duration(seconds: 1), ); @@ -618,6 +624,7 @@ void main() { test('negative aspect ratio -> 1.0', () { final value = VideoPlayerValue( + isInitialized: true, size: Size(640, -480), duration: Duration(seconds: 1), ); @@ -646,7 +653,7 @@ void main() { File(''), videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true)); await controller.initialize(); - expect(controller.videoPlayerOptions.mixWithOthers, true); + expect(controller.videoPlayerOptions!.mixWithOthers, true); }); } @@ -706,7 +713,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 +749,7 @@ class FakeVideoEventStream { int height; Duration duration; bool initWithError; - FakeEventsChannel eventsChannel; + late FakeEventsChannel eventsChannel; void onListen() { if (!initWithError) { @@ -764,7 +771,7 @@ class FakeEventsChannel { eventsMethodChannel.setMockMethodCallHandler(onMethodCall); } - MethodChannel eventsMethodChannel; + late MethodChannel eventsMethodChannel; VoidCallback onListen; Future onMethodCall(MethodCall call) { @@ -780,7 +787,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, @@ -789,11 +796,7 @@ class FakeEventsChannel { } void _sendMessage(ByteData data) { - // TODO(jackson): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - defaultBinaryMessenger.handlePlatformMessage( - eventsMethodChannel.name, data, (ByteData data) {}); + ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( + 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..d0c59bd05023 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,20 @@ +## 4.0.0 + +* **Breaking Changes**: + * Migrate to null-safety + * Update to latest Pigeon. This includes a breaking change to how the test logic is exposed. +* Add note about the `mixWithOthers` option being ignored on the web. +* Make DataSource's `uri` parameter nullable. +* `messages.dart` sets Dart `2.12`. + +## 3.0.0 + +* Version 3 only was published as nullsafety "previews". + +## 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..dc5237f2e151 100644 --- a/packages/video_player/video_player_platform_interface/lib/messages.dart +++ b/packages/video_player/video_player_platform_interface/lib/messages.dart @@ -1,39 +1,35 @@ -// Autogenerated from Pigeon (v0.1.7), do not edit directly. +// Autogenerated from Pigeon (v0.1.21), 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; +import 'package:flutter/services.dart'; + class TextureMessage { - int textureId; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + int? textureId; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['textureId'] = textureId; return pigeonMap; } - // ignore: unused_element - static TextureMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } - final TextureMessage result = TextureMessage(); - result.textureId = pigeonMap['textureId']; - return result; + static TextureMessage decode(Object message) { + final Map pigeonMap = message as Map; + return TextureMessage()..textureId = pigeonMap['textureId'] as int?; } } class CreateMessage { - String asset; - String uri; - String packageName; - String formatHint; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + String? asset; + String? uri; + String? packageName; + String? formatHint; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['asset'] = asset; pigeonMap['uri'] = uri; pigeonMap['packageName'] = packageName; @@ -41,514 +37,382 @@ class CreateMessage { return pigeonMap; } - // 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']; - result.packageName = pigeonMap['packageName']; - result.formatHint = pigeonMap['formatHint']; - return result; + static CreateMessage decode(Object message) { + final Map pigeonMap = message as Map; + return CreateMessage() + ..asset = pigeonMap['asset'] as String? + ..uri = pigeonMap['uri'] as String? + ..packageName = pigeonMap['packageName'] as String? + ..formatHint = pigeonMap['formatHint'] as String?; } } class LoopingMessage { - int textureId; - bool isLooping; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + int? textureId; + bool? isLooping; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['textureId'] = textureId; pigeonMap['isLooping'] = isLooping; return pigeonMap; } - // 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']; - return result; + static LoopingMessage decode(Object message) { + final Map pigeonMap = message as Map; + return LoopingMessage() + ..textureId = pigeonMap['textureId'] as int? + ..isLooping = pigeonMap['isLooping'] as bool?; } } class VolumeMessage { - int textureId; - double volume; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + int? textureId; + double? volume; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['textureId'] = textureId; pigeonMap['volume'] = volume; return pigeonMap; } - // 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']; - return result; + static VolumeMessage decode(Object message) { + final Map pigeonMap = message as Map; + return VolumeMessage() + ..textureId = pigeonMap['textureId'] as int? + ..volume = pigeonMap['volume'] as double?; } } class PlaybackSpeedMessage { - int textureId; - double speed; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + int? textureId; + double? speed; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['textureId'] = textureId; pigeonMap['speed'] = speed; return pigeonMap; } - // 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']; - return result; + static PlaybackSpeedMessage decode(Object message) { + final Map pigeonMap = message as Map; + return PlaybackSpeedMessage() + ..textureId = pigeonMap['textureId'] as int? + ..speed = pigeonMap['speed'] as double?; } } class PositionMessage { - int textureId; - int position; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + int? textureId; + int? position; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['textureId'] = textureId; pigeonMap['position'] = position; return pigeonMap; } - // 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']; - return result; + static PositionMessage decode(Object message) { + final Map pigeonMap = message as Map; + return PositionMessage() + ..textureId = pigeonMap['textureId'] as int? + ..position = pigeonMap['position'] as int?; } } class MixWithOthersMessage { - bool mixWithOthers; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + bool? mixWithOthers; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['mixWithOthers'] = mixWithOthers; return pigeonMap; } - // ignore: unused_element - static MixWithOthersMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } - final MixWithOthersMessage result = MixWithOthersMessage(); - result.mixWithOthers = pigeonMap['mixWithOthers']; - return result; + static MixWithOthersMessage decode(Object message) { + final Map pigeonMap = message as Map; + return MixWithOthersMessage() + ..mixWithOthers = pigeonMap['mixWithOthers'] as bool?; } } class VideoPlayerApi { Future initialize() async { - const BasicMessageChannel channel = BasicMessageChannel( + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.initialize', StandardMessageCodec()); - - final Map replyMap = await channel.send(null); + final Map? replyMap = + await channel.send(null) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future create(CreateMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.create', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { - return TextureMessage._fromMap(replyMap['result']); + return TextureMessage.decode(replyMap['result']!); } } Future dispose(TextureMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.dispose', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future setLooping(LoopingMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setLooping', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future setVolume(VolumeMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setVolume', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future setPlaybackSpeed(PlaybackSpeedMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future play(TextureMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.play', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future position(TextureMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { - return PositionMessage._fromMap(replyMap['result']); + return PositionMessage.decode(replyMap['result']!); } } Future seekTo(PositionMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.seekTo', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future pause(TextureMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.pause', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future setMixWithOthers(MixWithOthersMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } } - -abstract class TestHostVideoPlayerApi { - void initialize(); - TextureMessage create(CreateMessage arg); - void dispose(TextureMessage arg); - void setLooping(LoopingMessage arg); - void setVolume(VolumeMessage arg); - void setPlaybackSpeed(PlaybackSpeedMessage arg); - void play(TextureMessage arg); - PositionMessage position(TextureMessage arg); - void seekTo(PositionMessage arg); - void pause(TextureMessage arg); - void setMixWithOthers(MixWithOthersMessage arg); - static void setup(TestHostVideoPlayerApi api) { - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.initialize', - StandardMessageCodec()); - 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()}; - }); - } - { - 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 {}; - }); - } - { - 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 {}; - }); - } - { - 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 {}; - }); - } - { - 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 {}; - }); - } - { - 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 {}; - }); - } - { - 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()}; - }); - } - { - 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 {}; - }); - } - { - 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 {}; - }); - } - { - 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 {}; - }); - } - } -} 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/test.dart b/packages/video_player/video_player_platform_interface/lib/test.dart new file mode 100644 index 000000000000..457a838e8d24 --- /dev/null +++ b/packages/video_player/video_player_platform_interface/lib/test.dart @@ -0,0 +1,196 @@ +// Autogenerated from Pigeon (v0.1.21), 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.12 +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'messages.dart'; + +abstract class TestHostVideoPlayerApi { + void initialize(); + TextureMessage create(CreateMessage arg); + void dispose(TextureMessage arg); + void setLooping(LoopingMessage arg); + void setVolume(VolumeMessage arg); + void setPlaybackSpeed(PlaybackSpeedMessage arg); + void play(TextureMessage arg); + PositionMessage position(TextureMessage arg); + void seekTo(PositionMessage arg); + void pause(TextureMessage arg); + void setMixWithOthers(MixWithOthersMessage arg); + static void setup(TestHostVideoPlayerApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.initialize', + StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + api.initialize(); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.create', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.create was null. Expected CreateMessage.'); + final CreateMessage input = CreateMessage.decode(message!); + final TextureMessage output = api.create(input); + return {'result': output.encode()}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.dispose', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.dispose was null. Expected TextureMessage.'); + final TextureMessage input = TextureMessage.decode(message!); + api.dispose(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setLooping', + StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.setLooping was null. Expected LoopingMessage.'); + final LoopingMessage input = LoopingMessage.decode(message!); + api.setLooping(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setVolume', + StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.setVolume was null. Expected VolumeMessage.'); + final VolumeMessage input = VolumeMessage.decode(message!); + api.setVolume(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', + StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed was null. Expected PlaybackSpeedMessage.'); + final PlaybackSpeedMessage input = + PlaybackSpeedMessage.decode(message!); + api.setPlaybackSpeed(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.play', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.play was null. Expected TextureMessage.'); + final TextureMessage input = TextureMessage.decode(message!); + api.play(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.position was null. Expected TextureMessage.'); + final TextureMessage input = TextureMessage.decode(message!); + final PositionMessage output = api.position(input); + return {'result': output.encode()}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.seekTo', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.seekTo was null. Expected PositionMessage.'); + final PositionMessage input = PositionMessage.decode(message!); + api.seekTo(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.pause', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.pause was null. Expected TextureMessage.'); + final TextureMessage input = TextureMessage.decode(message!); + api.pause(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', + StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers was null. Expected MixWithOthersMessage.'); + final MixWithOthersMessage input = + MixWithOthersMessage.decode(message!); + api.setMixWithOthers(input); + return {}; + }); + } + } + } +} 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..77b34f46bc10 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) { @@ -346,6 +346,9 @@ class DurationRange { class VideoPlayerOptions { /// Set this to true to mix the video players audio with other audio sources. /// The default value is false + /// + /// Note: This option will be silently ignored in the web platform (there is + /// currently no way to implement this feature in this platform). final bool mixWithOthers; /// set additional optional player settings diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index cc3cd79f1f33..c85f483d041f 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -3,19 +3,18 @@ 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: 4.0.0 dependencies: flutter: sdk: flutter - meta: ^1.0.5 - -dev_dependencies: + meta: ^1.3.0 flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 + +dev_dependencies: + pedantic: ^1.10.0 environment: - sdk: ">=2.8.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.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..fae4b746bf05 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 @@ -6,20 +6,20 @@ import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; import 'package:video_player_platform_interface/messages.dart'; import 'package:video_player_platform_interface/method_channel_video_player.dart'; +import 'package:video_player_platform_interface/test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; class _ApiLogger implements TestHostVideoPlayerApi { final List log = []; - TextureMessage textureMessage; - CreateMessage createMessage; - PositionMessage positionMessage; - LoopingMessage loopingMessage; - VolumeMessage volumeMessage; - PlaybackSpeedMessage playbackSpeedMessage; - MixWithOthersMessage mixWithOthersMessage; + TextureMessage? textureMessage; + CreateMessage? createMessage; + PositionMessage? positionMessage; + LoopingMessage? loopingMessage; + VolumeMessage? volumeMessage; + PlaybackSpeedMessage? playbackSpeedMessage; + MixWithOthersMessage? mixWithOthersMessage; @override TextureMessage create(CreateMessage arg) { @@ -97,28 +97,11 @@ void main() { expect(VideoPlayerPlatform.instance, isInstanceOf()); }); - - test('Cannot be implemented with `implements`', () { - expect(() { - VideoPlayerPlatform.instance = ImplementsVideoPlayerPlatform(); - }, throwsA(isInstanceOf())); - }); - - test('Can be mocked with `implements`', () { - final ImplementsVideoPlayerPlatform mock = - ImplementsVideoPlayerPlatform(); - when(mock.isMock).thenReturn(true); - VideoPlayerPlatform.instance = mock; - }); - - test('Can be extended', () { - VideoPlayerPlatform.instance = ExtendsVideoPlayerPlatform(); - }); }); group('$MethodChannelVideoPlayer', () { final MethodChannelVideoPlayer player = MethodChannelVideoPlayer(); - _ApiLogger log; + late _ApiLogger log; setUp(() { log = _ApiLogger(); @@ -136,177 +119,158 @@ void main() { test('dispose', () async { await player.dispose(1); expect(log.log.last, 'dispose'); - expect(log.textureMessage.textureId, 1); + expect(log.textureMessage?.textureId, 1); }); test('create with asset', () async { - final int textureId = await player.create(DataSource( + final int? textureId = await player.create(DataSource( sourceType: DataSourceType.asset, asset: 'someAsset', package: 'somePackage', )); expect(log.log.last, 'create'); - expect(log.createMessage.asset, 'someAsset'); - expect(log.createMessage.packageName, 'somePackage'); + expect(log.createMessage?.asset, 'someAsset'); + expect(log.createMessage?.packageName, 'somePackage'); expect(textureId, 3); }); test('create with network', () async { - final int textureId = await player.create(DataSource( + final int? textureId = await player.create(DataSource( sourceType: DataSourceType.network, uri: 'someUri', formatHint: VideoFormat.dash, )); expect(log.log.last, 'create'); - expect(log.createMessage.uri, 'someUri'); - expect(log.createMessage.formatHint, 'dash'); + expect(log.createMessage?.uri, 'someUri'); + expect(log.createMessage?.formatHint, 'dash'); expect(textureId, 3); }); test('create with file', () async { - final int textureId = await player.create(DataSource( + final int? textureId = await player.create(DataSource( sourceType: DataSourceType.file, uri: 'someUri', )); expect(log.log.last, 'create'); - expect(log.createMessage.uri, 'someUri'); + expect(log.createMessage?.uri, 'someUri'); expect(textureId, 3); }); test('setLooping', () async { await player.setLooping(1, true); expect(log.log.last, 'setLooping'); - expect(log.loopingMessage.textureId, 1); - expect(log.loopingMessage.isLooping, true); + expect(log.loopingMessage?.textureId, 1); + expect(log.loopingMessage?.isLooping, true); }); test('play', () async { await player.play(1); expect(log.log.last, 'play'); - expect(log.textureMessage.textureId, 1); + expect(log.textureMessage?.textureId, 1); }); test('pause', () async { await player.pause(1); expect(log.log.last, 'pause'); - expect(log.textureMessage.textureId, 1); + expect(log.textureMessage?.textureId, 1); }); test('setMixWithOthers', () async { await player.setMixWithOthers(true); expect(log.log.last, 'setMixWithOthers'); - expect(log.mixWithOthersMessage.mixWithOthers, true); + expect(log.mixWithOthersMessage?.mixWithOthers, true); await player.setMixWithOthers(false); expect(log.log.last, 'setMixWithOthers'); - expect(log.mixWithOthersMessage.mixWithOthers, false); + expect(log.mixWithOthersMessage?.mixWithOthers, false); }); test('setVolume', () async { await player.setVolume(1, 0.7); expect(log.log.last, 'setVolume'); - expect(log.volumeMessage.textureId, 1); - expect(log.volumeMessage.volume, 0.7); + expect(log.volumeMessage?.textureId, 1); + expect(log.volumeMessage?.volume, 0.7); }); test('setPlaybackSpeed', () async { await player.setPlaybackSpeed(1, 1.5); expect(log.log.last, 'setPlaybackSpeed'); - expect(log.playbackSpeedMessage.textureId, 1); - expect(log.playbackSpeedMessage.speed, 1.5); + expect(log.playbackSpeedMessage?.textureId, 1); + expect(log.playbackSpeedMessage?.speed, 1.5); }); test('seekTo', () async { await player.seekTo(1, const Duration(milliseconds: 12345)); expect(log.log.last, 'seekTo'); - expect(log.positionMessage.textureId, 1); - expect(log.positionMessage.position, 12345); + expect(log.positionMessage?.textureId, 1); + expect(log.positionMessage?.position, 12345); }); test('getPosition', () async { final Duration position = await player.getPosition(1); expect(log.log.last, 'position'); - expect(log.textureMessage.textureId, 1); + expect(log.textureMessage?.textureId, 1); expect(position, const Duration(milliseconds: 234)); }); test('videoEventsFor', () async { - // TODO(cbenhagen): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - defaultBinaryMessenger.setMockMessageHandler( + ServicesBinding.instance?.defaultBinaryMessenger.setMockMessageHandler( "flutter.io/videoPlayer/videoEvents123", - (ByteData message) async { + (ByteData? message) async { final MethodCall methodCall = const StandardMethodCodec().decodeMethodCall(message); if (methodCall.method == 'listen') { - // TODO(cbenhagen): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - await defaultBinaryMessenger.handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'initialized', - 'duration': 98765, - 'width': 1920, - 'height': 1080, - }), - (ByteData data) {}); - - // TODO(cbenhagen): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - await defaultBinaryMessenger.handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'completed', - }), - (ByteData data) {}); - - // TODO(cbenhagen): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - await defaultBinaryMessenger.handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'bufferingUpdate', - 'values': >[ - [0, 1234], - [1235, 4000], - ], - }), - (ByteData data) {}); - - // TODO(cbenhagen): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - await defaultBinaryMessenger.handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'bufferingStart', - }), - (ByteData data) {}); - - // TODO(cbenhagen): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - await defaultBinaryMessenger.handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'bufferingEnd', - }), - (ByteData data) {}); + await ServicesBinding.instance?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'initialized', + 'duration': 98765, + 'width': 1920, + 'height': 1080, + }), + (ByteData? data) {}); + + await ServicesBinding.instance?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'completed', + }), + (ByteData? data) {}); + + await ServicesBinding.instance?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'bufferingUpdate', + 'values': >[ + [0, 1234], + [1235, 4000], + ], + }), + (ByteData? data) {}); + + await ServicesBinding.instance?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'bufferingStart', + }), + (ByteData? data) {}); + + await ServicesBinding.instance?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'bufferingEnd', + }), + (ByteData? data) {}); return const StandardMethodCodec().encodeSuccessEnvelope(null); } else if (methodCall.method == 'cancel') { @@ -343,8 +307,3 @@ void main() { }); }); } - -class ImplementsVideoPlayerPlatform extends Mock - implements VideoPlayerPlatform {} - -class ExtendsVideoPlayerPlatform extends VideoPlayerPlatform {} diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index d8bebd3ae4b6..63d4e10ef8da 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,13 @@ +## 2.0.0 + +* Migrate to null safety. +* Calling `setMixWithOthers()` now is silently ignored instead of throwing an exception. +* Fixed an issue where `isBuffering` was not updating on Web. + +## 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/README.md b/packages/video_player/video_player_web/README.md index 216b926bf26e..d44f738aeb66 100644 --- a/packages/video_player/video_player_web/README.md +++ b/packages/video_player/video_player_web/README.md @@ -3,13 +3,6 @@ The web implementation of [`video_player`][1]. -**Please set your constraint to `video_player_web: '>=0.1.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.1.y+z`. -Please use `video_player_web: '>=0.1.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage This package is the endorsed implementation of `video_player` for the web platform since version `0.10.5`, so it gets automatically added to your application by depending on `video_player: ^0.10.5`. @@ -35,6 +28,10 @@ The Web platform does **not** suppport `dart:io`, so attempts to create a `Video Playing videos without prior interaction with the site might be prohibited by the browser and lead to runtime errors. See also: https://goo.gl/xX8pDD. +## Mixing audio with other audio sources + +The `VideoPlayerOptions.mixWithOthers` option can't be implemented in web, at least at the moment. If you use this option it will be silently ignored. + ## Supported Formats **Different web browsers support different sets of video codecs.** diff --git a/packages/video_player/video_player_web/ios/video_player_web.podspec b/packages/video_player/video_player_web/ios/video_player_web.podspec deleted file mode 100644 index 5129b7c69032..000000000000 --- a/packages/video_player/video_player_web/ios/video_player_web.podspec +++ /dev/null @@ -1,20 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'video_player_web' - s.version = '0.0.1' - s.summary = 'No-op implementation of video_player_web web plugin to avoid build issues on iOS' - s.description = <<-DESC -temp fake video_player_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end \ No newline at end of file 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..18f9e5e58cd6 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,63 +99,78 @@ 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 Widget buildView(int textureId) { return HtmlElementView(viewType: 'videoPlayer-$textureId'); } + + /// Sets the audio mode to mix with other sources (ignored) + @override + Future setMixWithOthers(bool mixWithOthers) => Future.value(); } 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 +191,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 +289,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..7404896e04a4 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -1,10 +1,7 @@ name: video_player_web description: Web platform implementation of video_player. homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_web -# 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 flutter: plugin: @@ -18,16 +15,14 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 - video_player_platform_interface: ^2.2.0 + meta: ^1.3.0 + video_player_platform_interface: ^4.0.0 dev_dependencies: flutter_test: sdk: flutter - video_player: - path: ../video_player - pedantic: ^1.8.0 + pedantic: ^1.10.0 environment: - sdk: ">=2.8.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" 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..aee5b0350570 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 @@ -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. + @TestOn('browser') import 'dart:async'; @@ -8,22 +9,21 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:video_player/video_player.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; 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); @@ -98,7 +98,9 @@ void main() { await VideoPlayerPlatform.instance.setVolume(videoPlayerId, 0); await VideoPlayerPlatform.instance.play(videoPlayerId); - expect(eventStream, emitsError(isA())); + expect(() async { + await eventStream.last; + }, throwsA(isA())); }); test('can pause', () { @@ -136,5 +138,10 @@ void main() { expect(VideoPlayerPlatform.instance.buildView(textureId), isInstanceOf()); }); + + test('ignores setting mixWithOthers', () { + expect(VideoPlayerPlatform.instance.setMixWithOthers(true), completes); + expect(VideoPlayerPlatform.instance.setMixWithOthers(false), completes); + }); }); } diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index 8f1e238f6919..fb448d245127 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,17 @@ +## 2.0.0 + +* Migration to null-safety. +* Added support for progress tracking. +* Add section to the wiki explaining how to use Material components. +* Update integration test to workaround an iOS 14 issue with `evaluateJavascript`. +* Fix `onWebResourceError` on iOS. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Added `allowsInlineMediaPlayback` property. + +## 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..94000772ca71 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,14 +8,12 @@ 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) widget's Dartdoc for more details on how to use the widget. - - ## Android Platform Views The WebView is relying on [Platform Views](https://flutter.dev/docs/development/platform-integration/platform-views) to embed @@ -66,5 +64,10 @@ android { // Required by the Flutter WebView plugin. minSdkVersion 19 } - } +} ``` + +#### Enable Material Components for Android + +To use Material Components when the user interacts with input elements in the WebView, +follow the steps described in the [Enabling Material Components instructions](https://flutter.dev/docs/deployment/android#enabling-material-components). 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..4578c7e0d1fe 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 @@ -72,6 +72,11 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { return true; } + + @Override + public void onProgressChanged(WebView view, int progress) { + flutterWebViewClient.onLoadingProgress(progress); + } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @@ -367,11 +372,17 @@ private void applySettings(Map settings) { webView.setWebContentsDebuggingEnabled(debuggingEnabled); } break; + case "hasProgressTracking": + flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key); + break; case "gestureNavigationEnabled": break; 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/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java index 24926bfc4117..3590d67eb334 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -30,6 +30,7 @@ class FlutterWebViewClient { private static final String TAG = "FlutterWebViewClient"; private final MethodChannel methodChannel; private boolean hasNavigationDelegate; + boolean hasProgressTracking; FlutterWebViewClient(MethodChannel methodChannel) { this.methodChannel = methodChannel; @@ -125,6 +126,14 @@ private void onPageFinished(WebView view, String url) { methodChannel.invokeMethod("onPageFinished", args); } + void onLoadingProgress(int progress) { + if (hasProgressTracking) { + Map args = new HashMap<>(); + args.put("progress", progress); + methodChannel.invokeMethod("onProgress", args); + } + } + private void onWebResourceError( final int errorCode, final String description, final String failingUrl) { final Map args = new HashMap<>(); 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..50af77fe6c6e 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); }, @@ -144,7 +144,11 @@ void main() { await pageLoaded.future; expect(messagesReceived, isEmpty); - await controller.evaluateJavascript('Echo.postMessage("hello");'); + // Append a return value "1" in the end will prevent an iOS platform exception. + // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 + // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. + // https://github.com/flutter/flutter/issues/66318 + await controller.evaluateJavascript('Echo.postMessage("hello");1;'); expect(messagesReceived, equals(['hello'])); }); @@ -180,16 +184,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 +329,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 +639,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 +708,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..e7e7981150ca 100644 --- a/packages/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/example/lib/main.dart @@ -62,11 +62,12 @@ 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: [ + onProgress: (int progress) { + print("WebView is loading (progress : $progress%)"); + }, + javascriptChannels: { _toasterJavascriptChannel(context), - ].toSet(), + }, navigationDelegate: (NavigationRequest request) { if (request.url.startsWith('https://www.youtube.com/')) { print('blocking navigation to $request}'); @@ -107,7 +108,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 +148,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 +302,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..d7688b720f3f 100644 --- a/packages/webview_flutter/example/pubspec.yaml +++ b/packages/webview_flutter/example/pubspec.yaml @@ -2,12 +2,17 @@ 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-259.9.beta <3.0.0" dependencies: flutter: sdk: flutter webview_flutter: + # When depending on this package from a real application you should use: + # webview_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -17,9 +22,10 @@ dev_dependencies: sdk: flutter integration_test: path: ../../integration_test - pedantic: ^1.8.0 + pedantic: ^1.10.0 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/FLTWKProgressionDelegate.h b/packages/webview_flutter/ios/Classes/FLTWKProgressionDelegate.h new file mode 100644 index 000000000000..40139ead262c --- /dev/null +++ b/packages/webview_flutter/ios/Classes/FLTWKProgressionDelegate.h @@ -0,0 +1,19 @@ +// 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 +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWKProgressionDelegate : NSObject + +- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel; + +- (void)stopObservingProgress:(WKWebView *)webView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m b/packages/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m new file mode 100644 index 000000000000..ad864e6e1fd1 --- /dev/null +++ b/packages/webview_flutter/ios/Classes/FLTWKProgressionDelegate.m @@ -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. + +#import "FLTWKProgressionDelegate.h" + +NSString *const FLTWKEstimatedProgressKeyPath = @"estimatedProgress"; + +@implementation FLTWKProgressionDelegate { + FlutterMethodChannel *_methodChannel; +} + +- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _methodChannel = channel; + [webView addObserver:self + forKeyPath:FLTWKEstimatedProgressKeyPath + options:NSKeyValueObservingOptionNew + context:nil]; + } + return self; +} + +- (void)stopObservingProgress:(WKWebView *)webView { + [webView removeObserver:self forKeyPath:FLTWKEstimatedProgressKeyPath]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([keyPath isEqualToString:FLTWKEstimatedProgressKeyPath]) { + NSNumber *newValue = + change[NSKeyValueChangeNewKey] ?: 0; // newValue is anywhere between 0.0 and 1.0 + int newValueAsInt = [newValue floatValue] * 100; // Anywhere between 0 and 100 + [_methodChannel invokeMethod:@"onProgress" + arguments:@{@"progress" : [NSNumber numberWithInt:newValueAsInt]}]; + } +} + +@end diff --git a/packages/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/ios/Classes/FlutterWebView.m index 969e010913f3..5f2af3b8aae0 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/ios/Classes/FlutterWebView.m @@ -4,6 +4,7 @@ #import "FlutterWebView.h" #import "FLTWKNavigationDelegate.h" +#import "FLTWKProgressionDelegate.h" #import "JavaScriptChannelHandler.h" @implementation FLTWebViewFactory { @@ -64,6 +65,7 @@ @implementation FLTWebViewController { // The set of registered JavaScript channel names. NSMutableSet* _javaScriptChannelNames; FLTWKNavigationDelegate* _navigationDelegate; + FLTWKProgressionDelegate* _progressionDelegate; } - (instancetype)initWithFrame:(CGRect)frame @@ -119,6 +121,12 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } +- (void)dealloc { + if (_progressionDelegate != nil) { + [_progressionDelegate stopObservingProgress:_webView]; + } +} + - (UIView*)view { return _webView; } @@ -323,6 +331,13 @@ - (NSString*)applySettings:(NSDictionary*)settings { } else if ([key isEqualToString:@"hasNavigationDelegate"]) { NSNumber* hasDartNavigationDelegate = settings[key]; _navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue]; + } else if ([key isEqualToString:@"hasProgressTracking"]) { + NSNumber* hasProgressTrackingValue = settings[key]; + bool hasProgressTracking = [hasProgressTrackingValue boolValue]; + if (hasProgressTracking) { + _progressionDelegate = [[FLTWKProgressionDelegate alloc] initWithWebView:_webView + channel:_channel]; + } } else if ([key isEqualToString:@"debuggingEnabled"]) { // no-op debugging is always enabled on iOS. } else if ([key isEqualToString:@"gestureNavigationEnabled"]) { @@ -332,6 +347,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 6c991b14a76e..16b529d7090e 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); @@ -29,6 +30,10 @@ abstract class WebViewPlatformCallbacksHandler { /// Invoked by [WebViewPlatformController] when a page has finished loading. void onPageFinished(String url); + /// Invoked by [WebViewPlatformController] when a page is loading. + /// /// Only works when [WebSettings.hasProgressTracking] is set to `true`. + void onProgress(int progress); + /// Report web resource loading error to the host application. void onWebResourceError(WebResourceError error); } @@ -103,8 +108,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, @@ -131,7 +136,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; @@ -139,13 +144,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. @@ -176,7 +181,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"); @@ -194,7 +199,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"); } @@ -281,7 +286,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"); } @@ -337,7 +342,7 @@ class WebSetting { : _value = value, isPresent = true; - final T _value; + final T? _value; /// The setting's value. /// @@ -347,7 +352,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. @@ -358,7 +370,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; } @@ -380,21 +392,32 @@ class WebSettings { WebSettings({ this.javascriptMode, this.hasNavigationDelegate, + this.hasProgressTracking, 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 the [WebView] should track page loading progress. + /// See also: [WebViewPlatformCallbacksHandler.onProgress] to get the progress. + final bool? hasProgressTracking; /// 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. /// @@ -404,16 +427,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, hasProgressTracking: $hasProgressTracking, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)'; } } @@ -428,7 +451,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, @@ -437,12 +460,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. /// @@ -460,7 +483,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; @@ -475,7 +498,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. /// @@ -505,14 +528,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 348b225bb257..ef1ed51835b8 100644 --- a/packages/webview_flutter/lib/src/webview_method_channel.dart +++ b/packages/webview_flutter/lib/src/webview_method_channel.dart @@ -25,31 +25,35 @@ 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 'onProgress': + _platformCallbacksHandler.onProgress(call.arguments['progress']); return null; case 'onPageStarted': - _platformCallbacksHandler.onPageStarted(call.arguments['url']); + _platformCallbacksHandler.onPageStarted(call.arguments['url']!); return null; 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( @@ -71,7 +75,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { @override Future loadUrl( String url, - Map headers, + Map? headers, ) async { assert(url != null); return _channel.invokeMethod('loadUrl', { @@ -81,13 +85,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"); @@ -102,18 +108,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 @@ -129,7 +135,7 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { } @override - Future getTitle() => _channel.invokeMethod("getTitle"); + Future getTitle() => _channel.invokeMethod("getTitle"); @override Future scrollTo(int x, int y) { @@ -148,19 +154,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) { @@ -176,11 +184,14 @@ class MethodChannelWebViewPlatform implements WebViewPlatformController { map[key] = setting.value; } - _addIfNonNull('jsMode', settings.javascriptMode?.index); + _addIfNonNull('jsMode', settings!.javascriptMode?.index); _addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); + _addIfNonNull('hasProgressTracking', settings.hasProgressTracking); _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 2fdf639180a7..7e4f3d6ac079 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, @@ -142,6 +142,9 @@ typedef void PageStartedCallback(String url); /// Signature for when a [WebView] has finished loading a page. typedef void PageFinishedCallback(String url); +/// Signature for when a [WebView] is loading a page. +typedef void PageLoadingCallback(int progress); + /// Signature for when a [WebView] has failed to load a resource. typedef void WebResourceErrorCallback(WebResourceError error); @@ -172,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)); @@ -208,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, @@ -217,17 +220,20 @@ class WebView extends StatefulWidget { this.gestureRecognizers, this.onPageStarted, this.onPageFinished, + this.onProgress, this.onWebResourceError, this.debuggingEnabled = false, this.gestureNavigationEnabled = false, 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]. /// @@ -236,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; } @@ -257,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. /// @@ -272,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; @@ -307,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. /// @@ -331,10 +337,17 @@ 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; /// Invoked when a page starts loading. - final PageStartedCallback onPageStarted; + final PageStartedCallback? onPageStarted; /// Invoked when a page has finished loading. /// @@ -346,13 +359,16 @@ 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 page is loading. + final PageLoadingCallback? onProgress; /// 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. /// @@ -386,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. /// @@ -404,7 +420,7 @@ class _WebViewState extends State { final Completer _controller = Completer(); - _PlatformCallbacksHandler _platformCallbacksHandler; + late _PlatformCallbacksHandler _platformCallbacksHandler; @override Widget build(BuildContext context) { @@ -434,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); } } @@ -467,9 +483,11 @@ WebSettings _webSettingsFromWidget(WebView widget) { return WebSettings( javascriptMode: widget.javascriptMode, hasNavigationDelegate: widget.navigationDelegate != null, + hasProgressTracking: widget.onProgress != null, debuggingEnabled: widget.debuggingEnabled, gestureNavigationEnabled: widget.gestureNavigationEnabled, - userAgent: WebSetting.of(widget.userAgent), + allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, + userAgent: WebSetting.of(widget.userAgent), ); } @@ -478,23 +496,28 @@ WebSettings _clearUnchangedWebSettings( WebSettings currentValue, WebSettings newValue) { assert(currentValue.javascriptMode != null); assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != 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? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = WebSetting.absent(); if (currentValue.javascriptMode != newValue.javascriptMode) { javascriptMode = newValue.javascriptMode; } if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { hasNavigationDelegate = newValue.hasNavigationDelegate; } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { debuggingEnabled = newValue.debuggingEnabled; } @@ -505,12 +528,13 @@ WebSettings _clearUnchangedWebSettings( return WebSettings( javascriptMode: javascriptMode, hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, debuggingEnabled: debuggingEnabled, userAgent: userAgent, ); } -Set _extractChannelNames(Set channels) { +Set _extractChannelNames(Set? channels) { final Set channelNames = channels == null ? {} : channels.map((JavascriptChannel channel) => channel.name).toSet(); @@ -530,15 +554,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; } @@ -546,25 +573,31 @@ 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 onProgress(int progress) { + if (_widget.onProgress != null) { + _widget.onProgress!(progress); + } + } + 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; @@ -592,7 +625,7 @@ class WebViewController { final _PlatformCallbacksHandler _platformCallbacksHandler; - WebSettings _settings; + late WebSettings _settings; WebView _widget; @@ -606,7 +639,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); @@ -620,7 +653,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(); } @@ -688,7 +721,7 @@ class WebViewController { } Future _updateJavascriptChannels( - Set newChannels) async { + Set? newChannels) async { final Set currentChannels = _platformCallbacksHandler._javascriptChannels.keys.toSet(); final Set newChannelNames = _extractChannelNames(newChannels); @@ -727,10 +760,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 @@ -738,7 +767,7 @@ class WebViewController { } /// Returns the title of the currently loaded page. - Future getTitle() { + Future getTitle() { return _webViewPlatformController.getTitle(); } @@ -780,7 +809,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..bae19fd8b726 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 homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter +version: 2.0.0 environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.22.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <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 flutter: plugin: diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart index c7cf46a080d7..8ae6e625431d 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(); @@ -715,6 +692,62 @@ void main() { }); }); + group('$PageLoadingCallback', () { + testWidgets('onLoadingProgress is not null', (WidgetTester tester) async { + int? loadingProgress; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onProgress: (int progress) { + loadingProgress = progress; + }, + )); + + final FakePlatformWebView? platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView?.fakeOnProgressCallback(50); + + expect(loadingProgress, 50); + }); + + testWidgets('onLoadingProgress is null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + onProgress: null, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + // This is to test that it does not crash on a null callback. + platformWebView.fakeOnProgressCallback(50); + }); + + testWidgets('onLoadingProgress changed', (WidgetTester tester) async { + int? loadingProgress; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onProgress: (int progress) {}, + )); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onProgress: (int progress) { + loadingProgress = progress; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + platformWebView.fakeOnProgressCallback(50); + + expect(loadingProgress, 50); + }); + }); + group('navigationDelegate', () { testWidgets('hasNavigationDelegate', (WidgetTester tester) async { await tester.pumpWidget(const WebView( @@ -722,13 +755,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 +782,7 @@ void main() { })); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.hasNavigationDelegate, true); @@ -773,7 +807,7 @@ void main() { )); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.debuggingEnabled, true); }); @@ -782,7 +816,7 @@ void main() { await tester.pumpWidget(const WebView()); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.debuggingEnabled, false); }); @@ -792,7 +826,7 @@ void main() { await tester.pumpWidget(WebView(key: key)); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; await tester.pumpWidget(WebView( key: key, @@ -826,8 +860,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 +871,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 +888,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 +908,7 @@ void main() { )); final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformWebView.userAgent, isNull); @@ -892,9 +923,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 +945,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 +980,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 +1019,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 +1038,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 +1055,10 @@ class FakePlatformWebView { {'url': currentUrl}, )); - ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( channel.name, data, - (ByteData data) {}, + (ByteData? data) {}, ); } @@ -1045,14 +1070,26 @@ class FakePlatformWebView { {'url': currentUrl}, )); - ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( channel.name, data, - (ByteData data) {}, + (ByteData? data) {}, ); } - void _loadUrl(String url) { + void fakeOnProgressCallback(int progress) { + final StandardMethodCodec codec = const StandardMethodCodec(); + + final ByteData data = codec.encodeMethodCall(MethodCall( + 'onProgress', + {'progress': progress}, + )); + + ServicesBinding.instance!.defaultBinaryMessenger + .handlePlatformMessage(channel.name, data, (ByteData? data) {}); + } + + void _loadUrl(String? url) { history = history.sublist(0, currentPosition + 1); history.add(url); currentPosition++; @@ -1061,13 +1098,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 +1120,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 +1151,8 @@ class _FakeCookieManager { return Future.sync(() { return hadCookies; }); - break; } - return Future.sync(() => null); + return Future.sync(() => true); } void reset() { @@ -1125,26 +1161,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 +1189,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 +1215,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 +1239,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..c76009ec95fb 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md +++ b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md @@ -1,3 +1,20 @@ +## 2.0.0 + +* Migrate to null safety. + +## 1.0.4 + +* Android: Add Log warning for unsatisfied requirement(s) in Android P or higher. +* Android: Update Example project. + +## 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/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java index f74e4f0f75e1..e5e33af715ca 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java @@ -4,18 +4,31 @@ package io.flutter.plugins.wifi_info_flutter; +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.LocationManager; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; +import android.os.Build; +import androidx.core.content.ContextCompat; +import io.flutter.Log; /** Reports wifi information. */ class WifiInfoFlutter { private WifiManager wifiManager; + private Context context; + private static final String TAG = "WifiInfoFlutter"; - WifiInfoFlutter(WifiManager wifiManager) { + WifiInfoFlutter(WifiManager wifiManager, Context context) { this.wifiManager = wifiManager; + this.context = context; } String getWifiName() { + if (!checkPermissions()) { + return null; + } final WifiInfo wifiInfo = getWifiInfo(); String ssid = null; if (wifiInfo != null) ssid = wifiInfo.getSSID(); @@ -25,6 +38,9 @@ String getWifiName() { } String getWifiBSSID() { + if (!checkPermissions()) { + return null; + } final WifiInfo wifiInfo = getWifiInfo(); String bssid = null; if (wifiInfo != null) { @@ -53,4 +69,89 @@ String getWifiIPAddress() { private WifiInfo getWifiInfo() { return wifiManager == null ? null : wifiManager.getConnectionInfo(); } + + private Boolean checkPermissions() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return true; + } + + boolean grantedChangeWifiState = + ContextCompat.checkSelfPermission(context, Manifest.permission.CHANGE_WIFI_STATE) + == PackageManager.PERMISSION_GRANTED; + + boolean grantedAccessFine = + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED; + + boolean grantedAccessCoarse = + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) + == PackageManager.PERMISSION_GRANTED; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P + && !grantedChangeWifiState + && !grantedAccessFine + && !grantedAccessCoarse) { + Log.w( + TAG, + "Attempted to get Wi-Fi data that requires additional permission(s).\n" + + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android O, please ensure your app has one of the following permissions:\n" + + "- CHANGE_WIFI_STATE\n" + + "- ACCESS_FINE_LOCATION\n" + + "- ACCESS_COARSE_LOCATION\n" + + "For more information about Wi-Fi Restrictions in Android 8.0 and above, please consult the following link:\n" + + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); + return false; + } + + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P && !grantedChangeWifiState) { + Log.w( + TAG, + "Attempted to get Wi-Fi data that requires additional permission(s).\n" + + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android P, please ensure your app has the CHANGE_WIFI_STATE permission.\n" + + "For more information about Wi-Fi Restrictions in Android 9.0 and above, please consult the following link:\n" + + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); + return false; + } + + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P + && !grantedAccessFine + && !grantedAccessCoarse) { + Log.w( + TAG, + "Attempted to get Wi-Fi data that requires additional permission(s).\n" + + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android P, additional to CHANGE_WIFI_STATE please ensure your app has one of the following permissions too:\n" + + "- ACCESS_FINE_LOCATION\n" + + "- ACCESS_COARSE_LOCATION\n" + + "For more information about Wi-Fi Restrictions in Android 9.0 and above, please consult the following link:\n" + + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); + return false; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + && (!grantedAccessFine || !grantedChangeWifiState)) { + Log.w( + TAG, + "Attempted to get Wi-Fi data that requires additional permission(s).\n" + + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android Q, please ensure your app has the CHANGE_WIFI_STATE and ACCESS_FINE_LOCATION permission.\n" + + "For more information about Wi-Fi Restrictions in Android 10.0 and above, please consult the following link:\n" + + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); + return false; + } + + LocationManager locationManager = + (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + + boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !gpsEnabled) { + Log.w( + TAG, + "Attempted to get Wi-Fi data that requires additional permission(s).\n" + + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android P, please ensure Location services are enabled on the device (under Settings > Location).\n" + + "For more information about Wi-Fi Restrictions in Android 9.0 and above, please consult the following link:\n" + + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); + return false; + } + return true; + } } diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java index ea22e0bc88a5..1346617df53b 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java @@ -37,7 +37,7 @@ private void setupChannels(BinaryMessenger messenger, Context context) { final WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - final WifiInfoFlutter wifiInfoFlutter = new WifiInfoFlutter(wifiManager); + final WifiInfoFlutter wifiInfoFlutter = new WifiInfoFlutter(wifiManager, context); final WifiInfoFlutterMethodChannelHandler methodChannelHandler = new WifiInfoFlutterMethodChannelHandler(wifiInfoFlutter); diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/AndroidManifest.xml index 22158120a33f..bcecab36d14a 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,9 @@ + + + + diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart index 17a9e76ad946..b92e55028a45 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart @@ -42,7 +42,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; @@ -54,7 +54,7 @@ class _MyHomePageState extends State { String _connectionStatus = 'Unknown'; final Connectivity _connectivity = Connectivity(); final WifiInfo _wifiInfo = WifiInfo(); - StreamSubscription _connectivitySubscription; + late StreamSubscription _connectivitySubscription; @override void initState() { @@ -72,7 +72,7 @@ class _MyHomePageState extends State { // Platform messages are asynchronous, so we initialize in an async method. Future initConnectivity() async { - ConnectivityResult result; + late ConnectivityResult result; // Platform messages may fail, so we use a try/catch PlatformException. try { result = await _connectivity.checkConnectivity(); @@ -103,7 +103,7 @@ class _MyHomePageState extends State { Future _updateConnectionStatus(ConnectivityResult result) async { switch (result) { case ConnectivityResult.wifi: - String wifiName, wifiBSSID, wifiIP; + String? wifiName, wifiBSSID, wifiIP; try { if (!kIsWeb && Platform.isIOS) { @@ -114,12 +114,12 @@ class _MyHomePageState extends State { } if (status == LocationAuthorizationStatus.authorizedAlways || status == LocationAuthorizationStatus.authorizedWhenInUse) { - wifiName = await _connectivity.getWifiName(); + wifiName = await _wifiInfo.getWifiName(); } else { - wifiName = await _connectivity.getWifiName(); + wifiName = await _wifiInfo.getWifiName(); } } else { - wifiName = await _connectivity.getWifiName(); + wifiName = await _wifiInfo.getWifiName(); } } on PlatformException catch (e) { print(e.toString()); @@ -135,12 +135,12 @@ class _MyHomePageState extends State { } if (status == LocationAuthorizationStatus.authorizedAlways || status == LocationAuthorizationStatus.authorizedWhenInUse) { - wifiBSSID = await _connectivity.getWifiBSSID(); + wifiBSSID = await _wifiInfo.getWifiBSSID(); } else { - wifiBSSID = await _connectivity.getWifiBSSID(); + wifiBSSID = await _wifiInfo.getWifiBSSID(); } } else { - wifiBSSID = await _connectivity.getWifiBSSID(); + wifiBSSID = await _wifiInfo.getWifiBSSID(); } } on PlatformException catch (e) { print(e.toString()); @@ -148,7 +148,7 @@ class _MyHomePageState extends State { } try { - wifiIP = await _connectivity.getWifiIP(); + wifiIP = await _wifiInfo.getWifiIP(); } on PlatformException catch (e) { print(e.toString()); wifiIP = "Failed to get Wifi IP"; diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml index 7ebc1573ddcf..bd424859abf2 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml @@ -3,15 +3,19 @@ description: Demonstrates how to use the wifi_info_flutter plugin. publish_to: 'none' environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" dependencies: - connectivity: 0.4.9+3 + connectivity: ^3.0.0 flutter: sdk: flutter wifi_info_flutter: + # When depending on this package from a real application you should use: + # wifi_info_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - cupertino_icons: ^1.0.0 dev_dependencies: integration_test: diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test/wifi_info_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test/wifi_info_test.dart index 103dc54a1eaa..6fe4737ae7a1 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test/wifi_info_test.dart +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test/wifi_info_test.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. +// @dart = 2.9 import 'dart:io'; import 'package:integration_test/integration_test.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/test/integration_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/test/integration_test.dart index 8a77ec87bbac..d59272c77431 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/test/integration_test.dart +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/test/integration_test.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. +// @dart = 2.9 import 'dart:convert'; import 'dart:io'; import 'package:flutter_driver/flutter_driver.dart'; diff --git a/packages/wifi_info_flutter/wifi_info_flutter/integration_test/wifi_info_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/integration_test/wifi_info_test.dart index 103dc54a1eaa..4760b88d9019 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/integration_test/wifi_info_test.dart +++ b/packages/wifi_info_flutter/wifi_info_flutter/integration_test/wifi_info_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 'package:integration_test/integration_test.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart b/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart index 1183bf6c74bf..a2a69d161f5a 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart +++ b/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart @@ -13,19 +13,14 @@ export 'package:wifi_info_flutter_platform_interface/wifi_info_flutter_platform_ /// Checks WI-FI status and more. class WifiInfo { + WifiInfo._(); + /// Constructs a singleton instance of [WifiInfo]. /// /// [WifiInfo] is designed to work as a singleton. - factory WifiInfo() { - if (_singleton == null) { - _singleton = WifiInfo._(); - } - return _singleton; - } - - WifiInfo._(); + factory WifiInfo() => _singleton; - static WifiInfo _singleton; + static final WifiInfo _singleton = WifiInfo._(); static WifiInfoFlutterPlatform get _platform => WifiInfoFlutterPlatform.instance; @@ -36,7 +31,7 @@ class WifiInfo { /// /// From android 8.0 onwards the GPS must be ON (high accuracy) /// in order to be able to obtain the SSID. - Future getWifiName() { + Future getWifiName() { return _platform.getWifiName(); } @@ -46,12 +41,12 @@ class WifiInfo { /// /// From Android 8.0 onwards the GPS must be ON (high accuracy) /// in order to be able to obtain the BSSID. - Future getWifiBSSID() { + Future getWifiBSSID() { return _platform.getWifiBSSID(); } /// Obtains the IP address of the connected wifi network - Future getWifiIP() { + Future getWifiIP() { return _platform.getWifiIP(); } diff --git a/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml index 74ee9f074cb5..7b58d481f6c7 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml +++ b/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml @@ -1,20 +1,18 @@ name: wifi_info_flutter description: A new flutter plugin project. -version: 1.0.1 homepage: https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter/wifi_info_flutter +version: 2.0.0 environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.20.0 <2.0.0" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - wifi_info_flutter_platform_interface: ^1.0.0 + wifi_info_flutter_platform_interface: ^2.0.0 dev_dependencies: - mockito: ^4.1.1 - plugin_platform_interface: ^1.0.0 integration_test: path: ../../integration_test flutter_test: diff --git a/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart index a3a55170bce5..19e84f696f7f 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart +++ b/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.dart @@ -2,13 +2,9 @@ // 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:wifi_info_flutter/wifi_info_flutter.dart'; import 'package:wifi_info_flutter_platform_interface/wifi_info_flutter_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:mockito/mockito.dart'; const String kWifiNameResult = '1337wifi'; const String kWifiBSSIDResult = 'c0:ff:33:c0:d3:55'; @@ -20,7 +16,7 @@ const LocationAuthorizationStatus kGetLocationResult = void main() { group('$WifiInfo', () { - WifiInfo wifiInfo; + late WifiInfo wifiInfo; MockWifiInfoFlutterPlatform fakePlatform; setUp(() async { @@ -30,17 +26,17 @@ void main() { }); test('getWifiName', () async { - String result = await wifiInfo.getWifiName(); + String? result = await wifiInfo.getWifiName(); expect(result, kWifiNameResult); }); test('getWifiBSSID', () async { - String result = await wifiInfo.getWifiBSSID(); + String? result = await wifiInfo.getWifiBSSID(); expect(result, kWifiBSSIDResult); }); test('getWifiIP', () async { - String result = await wifiInfo.getWifiIP(); + String? result = await wifiInfo.getWifiIP(); expect(result, kWifiIpAddressResult); }); @@ -58,27 +54,30 @@ void main() { }); } -class MockWifiInfoFlutterPlatform extends Mock - with MockPlatformInterfaceMixin - implements WifiInfoFlutterPlatform { - Future getWifiName() async { +class MockWifiInfoFlutterPlatform extends WifiInfoFlutterPlatform { + @override + Future getWifiName() async { return kWifiNameResult; } - Future getWifiBSSID() async { + @override + Future getWifiBSSID() async { return kWifiBSSIDResult; } - Future getWifiIP() async { + @override + Future getWifiIP() async { return kWifiIpAddressResult; } + @override Future requestLocationServiceAuthorization({ bool requestAlwaysLocationUsage = false, }) async { return kRequestLocationResult; } + @override Future getLocationServiceAuthorization() async { return kGetLocationResult; } 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..cb770cb2d279 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,11 @@ +## 2.0.0 + +* Migrate to null safety. + +## 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/lib/src/method_channel_wifi_info_flutter.dart b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/method_channel_wifi_info_flutter.dart index ef390c3cde1e..ba422ecd7565 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/method_channel_wifi_info_flutter.dart +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/method_channel_wifi_info_flutter.dart @@ -17,17 +17,17 @@ class MethodChannelWifiInfoFlutter extends WifiInfoFlutterPlatform { MethodChannel('plugins.flutter.io/wifi_info_flutter'); @override - Future getWifiName() async { + Future getWifiName() async { return methodChannel.invokeMethod('wifiName'); } @override - Future getWifiBSSID() { + Future getWifiBSSID() { return methodChannel.invokeMethod('wifiBSSID'); } @override - Future getWifiIP() { + Future getWifiIP() { return methodChannel.invokeMethod('wifiIPAddress'); } @@ -50,7 +50,7 @@ class MethodChannelWifiInfoFlutter extends WifiInfoFlutterPlatform { } /// Convert a String to a LocationAuthorizationStatus value. -LocationAuthorizationStatus _parseLocationAuthorizationStatus(String result) { +LocationAuthorizationStatus _parseLocationAuthorizationStatus(String? result) { return LocationAuthorizationStatus.values.firstWhere( (LocationAuthorizationStatus status) => result == describeEnum(status), orElse: () => LocationAuthorizationStatus.unknown, diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/wifi_info_flutter_platform_interface.dart b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/wifi_info_flutter_platform_interface.dart index 85034b2cbbff..5115af7e56eb 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/wifi_info_flutter_platform_interface.dart +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/wifi_info_flutter_platform_interface.dart @@ -43,17 +43,17 @@ abstract class WifiInfoFlutterPlatform 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/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml index 68d516d515a8..8e2eff392df7 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,20 +1,20 @@ name: wifi_info_flutter_platform_interface description: A common platform interface for the wifi_info_flutter plugin. -version: 1.0.0 +version: 2.0.0 # 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" + sdk: ">=2.12.0-259.9.beta <3.0.0" + flutter: ">=1.17.0" dependencies: - plugin_platform_interface: ^1.0.3 + plugin_platform_interface: ">=1.0.0 <3.0.0" flutter: sdk: flutter dev_dependencies: - pedantic: ^1.9.2 + pedantic: ^1.10.0 flutter_test: sdk: flutter diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/test/method_channel_wifi_info_flutter_test.dart b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/test/method_channel_wifi_info_flutter_test.dart index b48b2c4110bb..33733170c9fb 100644 --- a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/test/method_channel_wifi_info_flutter_test.dart +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/test/method_channel_wifi_info_flutter_test.dart @@ -12,7 +12,7 @@ void main() { group('$MethodChannelWifiInfoFlutter', () { final List log = []; - MethodChannelWifiInfoFlutter methodChannelWifiInfoFlutter; + late MethodChannelWifiInfoFlutter methodChannelWifiInfoFlutter; setUp(() async { methodChannelWifiInfoFlutter = MethodChannelWifiInfoFlutter(); @@ -39,7 +39,7 @@ void main() { }); test('getWifiName', () async { - final String result = await methodChannelWifiInfoFlutter.getWifiName(); + final String? result = await methodChannelWifiInfoFlutter.getWifiName(); expect(result, '1337wifi'); expect( log, @@ -53,7 +53,7 @@ void main() { }); test('getWifiBSSID', () async { - final String result = await methodChannelWifiInfoFlutter.getWifiBSSID(); + final String? result = await methodChannelWifiInfoFlutter.getWifiBSSID(); expect(result, 'c0:ff:33:c0:d3:55'); expect( log, @@ -67,7 +67,7 @@ void main() { }); test('getWifiIP', () async { - final String result = await methodChannelWifiInfoFlutter.getWifiIP(); + final String? result = await methodChannelWifiInfoFlutter.getWifiIP(); expect(result, '127.0.0.1'); expect( log, diff --git a/script/build_all_plugins_app.sh b/script/build_all_plugins_app.sh index f1f68ddbe985..0cd6f4888e79 100755 --- a/script/build_all_plugins_app.sh +++ b/script/build_all_plugins_app.sh @@ -1,48 +1,51 @@ #!/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 +# This list should be kept as short as possible, and things should remain here +# only as long as necessary, since in general the goal is for all of the latest +# versions of plugins to be mutually compatible. +# +# An example use case for this list would be to temporarily add plugins while +# updating multiple plugins for a breaking change in a common dependency in +# cases where using a relaxed version constraint isn't possible. readonly EXCLUDED_PLUGINS_LIST=( - "connectivity_macos" - "connectivity_platform_interface" - "connectivity_web" - "extension_google_sign_in_as_googleapis_auth" - "flutter_plugin_android_lifecycle" - "google_maps_flutter_platform_interface" - "google_maps_flutter_web" - "google_sign_in_platform_interface" - "google_sign_in_web" - "image_picker_platform_interface" - "instrumentation_adapter" - "path_provider_linux" - "path_provider_macos" - "path_provider_platform_interface" - "path_provider_web" - "plugin_platform_interface" - "shared_preferences_linux" - "shared_preferences_macos" - "shared_preferences_platform_interface" - "shared_preferences_web" - "shared_preferences_windows" - "url_launcher_linux" - "url_launcher_macos" - "url_launcher_platform_interface" - "url_launcher_web" - "video_player_platform_interface" - "video_player_web" + # "file_selector" # currently out of sync with camera + # "flutter_plugin_android_lifecycle" + # "image_picker" + # "local_auth" # flutter_plugin_android_lifecycle conflict + "plugin_platform_interface" # This should never be a direct app dependency. ) # 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, and conflicting plugins otherwise. +if [ "$CHANNEL" == "stable" ]; then + ALL_EXCLUDED=("$EXCLUDED,$EXCLUDED_PLUGINS_FROM_STABLE") +else + ALL_EXCLUDED=("$EXCLUDED,$EXCLUDED_PLUGINS_FOR_NNBD") +fi + +echo "Excluding the following plugins: $ALL_EXCLUDED" + +(cd "$REPO_DIR" && plugin_tools all-plugins-app --exclude $ALL_EXCLUDED) function error() { echo "$@" 1>&2 diff --git a/script/check_publish.sh b/script/check_publish.sh index 2e53fc80cb47..c92de4be2e08 100755 --- a/script/check_publish.sh +++ b/script/check_publish.sh @@ -10,33 +10,9 @@ readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" source "$SCRIPT_DIR/common.sh" -function check_publish() { - local failures=() - for dir in $(pub global run flutter_plugin_tools list --plugins="$1"); do - local package_name=$(basename "$dir") - - echo "Checking that $package_name can be published." - if [[ $(cd "$dir" && cat pubspec.yaml | grep -E "^publish_to: none") ]]; then - echo "Package $package_name is marked as unpublishable. Skipping." - elif (cd "$dir" && flutter pub publish -- --dry-run > /dev/null); then - echo "Package $package_name is able to be published." - else - error "Unable to publish $package_name" - failures=("${failures[@]}" "$package_name") - fi - done - if [[ "${#failures[@]}" != 0 ]]; then - error "FAIL: The following ${#failures[@]} package(s) failed the publishing check:" - for failure in "${failures[@]}"; do - error "$failure" - done - fi - return "${#failures[@]}" -} - # Sets CHANGED_PACKAGE_LIST and CHANGED_PACKAGES check_changed_packages if [[ "${#CHANGED_PACKAGE_LIST[@]}" != 0 ]]; then - check_publish "${CHANGED_PACKAGES}" + plugin_tools publish-check --plugins="${CHANGED_PACKAGES}" fi diff --git a/script/common.sh b/script/common.sh index 7950a3ea71cd..28c37540af88 100644 --- a/script/common.sh +++ b/script/common.sh @@ -45,3 +45,9 @@ function check_changed_packages() { fi return 0 } + +# Runs the plugin tools from the plugin_tools git submodule. +function plugin_tools() { + (pushd "$REPO_DIR/script/tool" && dart pub get && popd) >/dev/null + dart run "$REPO_DIR/script/tool/lib/src/main.dart" "$@" +} diff --git a/script/incremental_build.sh b/script/incremental_build.sh index f89bc1d0e5c9..bc41ebd3c70d 100755 --- a/script/incremental_build.sh +++ b/script/incremental_build.sh @@ -5,11 +5,14 @@ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" source "$SCRIPT_DIR/common.sh" - -if [ "$(expr substr $(uname -s) 1 5)" == "MINGW" ]; then - PUB=pub.bat -else - PUB=pub +source "$SCRIPT_DIR/nnbd_plugins.sh" + +# 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 because stable does not yet support NNBD: $ALL_EXCLUDED" fi # Plugins that deliberately use their own analysis_options.yaml. @@ -21,6 +24,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 +43,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" && plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) else # Sets CHANGED_PACKAGES check_changed_packages @@ -47,11 +51,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" && 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" && 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..fb5f8eac44e9 --- /dev/null +++ b/script/nnbd_plugins.sh @@ -0,0 +1,49 @@ +#!/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_alarm_manager" + "android_intent" + "battery" + "camera" + "camera_platform_interface" + "connectivity" + "cross_file" + "device_info" + "espresso" + "file_selector" + "flutter_plugin_android_lifecycle" + "flutter_webview" + "google_maps_flutter" + "google_sign_in" + "image_picker" + "ios_platform_images" + "local_auth" + "path_provider" + "package_info" + "plugin_platform_interface" + "quick_actions" + "sensors" + "share" + "shared_preferences" + "url_launcher" + "video_player" + "webview_flutter" + "wifi_info_flutter" + "in_app_purchase" +) + +# This list contains the list of plugins that have *not* been +# migrated to nnbd, and conflict with those that have when +# building the all plugins app. This list should be kept empty. + +readonly NON_NNBD_PLUGINS_LIST=( + "extension_google_sign_in_as_googleapis_auth" + "google_maps_flutter_web" # Not yet migrated. +) + +export EXCLUDED_PLUGINS_FROM_STABLE=$(IFS=, ; echo "${NNBD_PLUGINS_LIST[*]}") +export EXCLUDED_PLUGINS_FOR_NNBD=$(IFS=, ; echo "${NON_NNBD_PLUGINS_LIST[*]}") diff --git a/script/tool/README.md b/script/tool/README.md new file mode 100644 index 000000000000..162ca0d98a74 --- /dev/null +++ b/script/tool/README.md @@ -0,0 +1,8 @@ +# Flutter Plugin Tools + +To run the tool: + +```sh +dart pub get +dart run lib/src/main.dart +``` diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart new file mode 100644 index 000000000000..8cd57fa0b338 --- /dev/null +++ b/script/tool/lib/src/analyze_command.dart @@ -0,0 +1,94 @@ +// 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 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class AnalyzeCommand extends PluginCommand { + AnalyzeCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addMultiOption(_customAnalysisFlag, + help: + 'Directories (comma seperated) that are allowed to have their own analysis options.', + defaultsTo: []); + } + + static const String _customAnalysisFlag = 'custom-analysis'; + + @override + final String name = 'analyze'; + + @override + final String description = 'Analyzes all packages using package:tuneup.\n\n' + 'This command requires "pub" and "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + + print('Verifying analysis settings...'); + final List files = packagesDir.listSync(recursive: true); + for (final FileSystemEntity file in files) { + if (file.basename != 'analysis_options.yaml' && + file.basename != '.analysis_options') { + continue; + } + + final bool whitelisted = argResults[_customAnalysisFlag].any( + (String directory) => + p.isWithin(p.join(packagesDir.path, directory), file.path)); + if (whitelisted) { + continue; + } + + print('Found an extra analysis_options.yaml in ${file.absolute.path}.'); + print( + 'If this was deliberate, pass the package to the analyze command with the --$_customAnalysisFlag flag and try again.'); + throw ToolExit(1); + } + + print('Activating tuneup package...'); + await processRunner.runAndStream( + 'pub', ['global', 'activate', 'tuneup'], + workingDir: packagesDir, exitOnError: true); + + await for (Directory package in getPackages()) { + if (isFlutterPackage(package, fileSystem)) { + await processRunner.runAndStream('flutter', ['packages', 'get'], + workingDir: package, exitOnError: true); + } else { + await processRunner.runAndStream('pub', ['get'], + workingDir: package, exitOnError: true); + } + } + + final List failingPackages = []; + await for (Directory package in getPlugins()) { + final int exitCode = await processRunner.runAndStream( + 'pub', ['global', 'run', 'tuneup', 'check'], + workingDir: package); + if (exitCode != 0) { + failingPackages.add(p.basename(package.path)); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print('The following packages have analyzer errors (see above):'); + failingPackages.forEach((String package) { + print(' * $package'); + }); + throw ToolExit(1); + } + + print('No analyzer errors found!'); + } +} diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart new file mode 100644 index 000000000000..53da9086abaa --- /dev/null +++ b/script/tool/lib/src/build_examples_command.dart @@ -0,0 +1,188 @@ +// 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:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common.dart'; + +class BuildExamplesCommand extends PluginCommand { + BuildExamplesCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag(kLinux, defaultsTo: false); + argParser.addFlag(kMacos, defaultsTo: false); + argParser.addFlag(kWindows, defaultsTo: false); + argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); + argParser.addFlag(kApk); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + @override + final String name = 'build-examples'; + + @override + final String description = + 'Builds all example apps (IPA for iOS and APK for Android).\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + if (!argResults[kIpa] && + !argResults[kApk] && + !argResults[kLinux] && + !argResults[kMacos] && + !argResults[kWindows]) { + print( + 'None of --linux, --macos, --windows, --apk nor --ipa were specified, ' + 'so not building anything.'); + return; + } + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + final String enableExperiment = argResults[kEnableExperiment]; + + checkSharding(); + final List failingPackages = []; + await for (Directory plugin in getPlugins()) { + for (Directory example in getExamplesForPlugin(plugin)) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + + if (argResults[kLinux]) { + print('\nBUILDING Linux for $packageName'); + if (isLinuxPlugin(plugin, fileSystem)) { + int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kLinux, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (linux)'); + } + } else { + print('Linux is not supported by this plugin'); + } + } + + if (argResults[kMacos]) { + print('\nBUILDING macOS for $packageName'); + if (isMacOsPlugin(plugin, fileSystem)) { + // TODO(https://github.com/flutter/flutter/issues/46236): + // Builing macos without running flutter pub get first results + // in an error. + int exitCode = await processRunner.runAndStream( + flutterCommand, ['pub', 'get'], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (macos)'); + } else { + exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kMacos, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (macos)'); + } + } + } else { + print('macOS is not supported by this plugin'); + } + } + + if (argResults[kWindows]) { + print('\nBUILDING Windows for $packageName'); + if (isWindowsPlugin(plugin, fileSystem)) { + int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kWindows, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (windows)'); + } + } else { + print('Windows is not supported by this plugin'); + } + } + + if (argResults[kIpa]) { + print('\nBUILDING IPA for $packageName'); + if (isIosPlugin(plugin, fileSystem)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (ipa)'); + } + } else { + print('iOS is not supported by this plugin'); + } + } + + if (argResults[kApk]) { + print('\nBUILDING APK for $packageName'); + if (isAndroidPlugin(plugin, fileSystem)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'apk', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (apk)'); + } + } else { + print('Android is not supported by this plugin'); + } + } + } + } + print('\n\n'); + + if (failingPackages.isNotEmpty) { + print('The following build are failing (see above for details):'); + for (String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + + print('All builds successful!'); + } +} diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart new file mode 100644 index 000000000000..08622df281b4 --- /dev/null +++ b/script/tool/lib/src/common.dart @@ -0,0 +1,468 @@ +// 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:io' as io; +import 'dart:math'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; + +typedef void Print(Object object); + +/// Key for windows platform. +const String kWindows = 'windows'; + +/// Key for macos platform. +const String kMacos = 'macos'; + +/// Key for linux platform. +const String kLinux = 'linux'; + +/// Key for IPA (iOS) platform. +const String kIos = 'ios'; + +/// Key for APK (Android) platform. +const String kAndroid = 'android'; + +/// Key for Web platform. +const String kWeb = 'web'; + +/// Key for IPA. +const String kIpa = 'ipa'; + +/// Key for APK. +const String kApk = 'apk'; + +/// Key for enable experiment. +const String kEnableExperiment = 'enable-experiment'; + +/// Returns whether the given directory contains a Flutter package. +bool isFlutterPackage(FileSystemEntity entity, FileSystem fileSystem) { + if (entity == null || entity is! Directory) { + return false; + } + + try { + final File pubspecFile = + fileSystem.file(p.join(entity.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final YamlMap dependencies = pubspecYaml['dependencies']; + if (dependencies == null) { + return false; + } + return dependencies.containsKey('flutter'); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Returns whether the given directory contains a Flutter [platform] plugin. +/// +/// It checks this by looking for the following pattern in the pubspec: +/// +/// flutter: +/// plugin: +/// platforms: +/// [platform]: +bool pluginSupportsPlatform( + String platform, FileSystemEntity entity, FileSystem fileSystem) { + assert(platform == kIos || + platform == kAndroid || + platform == kWeb || + platform == kMacos || + platform == kWindows || + platform == kLinux); + if (entity == null || entity is! Directory) { + return false; + } + + try { + final File pubspecFile = + fileSystem.file(p.join(entity.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final YamlMap flutterSection = pubspecYaml['flutter']; + if (flutterSection == null) { + return false; + } + final YamlMap pluginSection = flutterSection['plugin']; + if (pluginSection == null) { + return false; + } + final YamlMap platforms = pluginSection['platforms']; + if (platforms == null) { + // Legacy plugin specs are assumed to support iOS and Android. + if (!pluginSection.containsKey('platforms')) { + return platform == kIos || platform == kAndroid; + } + return false; + } + return platforms.containsKey(platform); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Returns whether the given directory contains a Flutter Android plugin. +bool isAndroidPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kAndroid, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter iOS plugin. +bool isIosPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kIos, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter web plugin. +bool isWebPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kWeb, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter Windows plugin. +bool isWindowsPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kWindows, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter macOS plugin. +bool isMacOsPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kMacos, entity, fileSystem); +} + +/// Returns whether the given directory contains a Flutter linux plugin. +bool isLinuxPlugin(FileSystemEntity entity, FileSystem fileSystem) { + return pluginSupportsPlatform(kLinux, entity, fileSystem); +} + +/// Error thrown when a command needs to exit with a non-zero exit code. +class ToolExit extends Error { + ToolExit(this.exitCode); + + final int exitCode; +} + +abstract class PluginCommand extends Command { + PluginCommand( + this.packagesDir, + this.fileSystem, { + this.processRunner = const ProcessRunner(), + }) { + argParser.addMultiOption( + _pluginsArg, + splitCommas: true, + help: + 'Specifies which plugins the command should run on (before sharding).', + valueHelp: 'plugin1,plugin2,...', + ); + argParser.addOption( + _shardIndexArg, + help: 'Specifies the zero-based index of the shard to ' + 'which the command applies.', + valueHelp: 'i', + defaultsTo: '0', + ); + argParser.addOption( + _shardCountArg, + help: 'Specifies the number of shards into which plugins are divided.', + valueHelp: 'n', + defaultsTo: '1', + ); + argParser.addMultiOption( + _excludeArg, + abbr: 'e', + help: 'Exclude packages from this command.', + defaultsTo: [], + ); + } + + static const String _pluginsArg = 'plugins'; + static const String _shardIndexArg = 'shardIndex'; + static const String _shardCountArg = 'shardCount'; + static const String _excludeArg = 'exclude'; + + /// The directory containing the plugin packages. + final Directory packagesDir; + + /// The file system. + /// + /// This can be overridden for testing. + final FileSystem fileSystem; + + /// The process runner. + /// + /// This can be overridden for testing. + final ProcessRunner processRunner; + + int _shardIndex; + int _shardCount; + + int get shardIndex { + if (_shardIndex == null) { + checkSharding(); + } + return _shardIndex; + } + + int get shardCount { + if (_shardCount == null) { + checkSharding(); + } + return _shardCount; + } + + void checkSharding() { + final int shardIndex = int.tryParse(argResults[_shardIndexArg]); + final int shardCount = int.tryParse(argResults[_shardCountArg]); + if (shardIndex == null) { + usageException('$_shardIndexArg must be an integer'); + } + if (shardCount == null) { + usageException('$_shardCountArg must be an integer'); + } + if (shardCount < 1) { + usageException('$_shardCountArg must be positive'); + } + if (shardIndex < 0 || shardCount <= shardIndex) { + usageException( + '$_shardIndexArg must be in the half-open range [0..$shardCount['); + } + _shardIndex = shardIndex; + _shardCount = shardCount; + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution. + Stream getPlugins() async* { + // To avoid assuming consistency of `Directory.list` across command + // invocations, we collect and sort the plugin folders before sharding. + // This is considered an implementation detail which is why the API still + // uses streams. + final List allPlugins = await _getAllPlugins().toList(); + allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); + // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. + // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. + // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. + final int shardSize = allPlugins.length ~/ shardCount + + (allPlugins.length % shardCount == 0 ? 0 : 1); + final int start = min(shardIndex * shardSize, allPlugins.length); + final int end = min(start + shardSize, allPlugins.length); + + for (Directory plugin in allPlugins.sublist(start, end)) { + yield plugin; + } + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution, assuming there is only one shard. + /// + /// Plugin packages can exist in one of two places relative to the packages + /// directory. + /// + /// 1. As a Dart package in a directory which is a direct child of the + /// packages directory. This is a plugin where all of the implementations + /// exist in a single Dart package. + /// 2. Several plugin packages may live in a directory which is a direct + /// child of the packages directory. This directory groups several Dart + /// packages which implement a single plugin. This directory contains a + /// "client library" package, which declares the API for the plugin, as + /// well as one or more platform-specific implementations. + Stream _getAllPlugins() async* { + final Set plugins = Set.from(argResults[_pluginsArg]); + final Set excludedPlugins = + Set.from(argResults[_excludeArg]); + + await for (FileSystemEntity entity + in packagesDir.list(followLinks: false)) { + // A top-level Dart package is a plugin package. + if (_isDartPackage(entity)) { + if (!excludedPlugins.contains(entity.basename) && + (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { + yield entity; + } + } else if (entity is Directory) { + // Look for Dart packages under this top-level directory. + await for (FileSystemEntity subdir in entity.list(followLinks: false)) { + if (_isDartPackage(subdir)) { + // If --plugin=my_plugin is passed, then match all federated + // plugins under 'my_plugin'. Also match if the exact plugin is + // passed. + final String relativePath = + p.relative(subdir.path, from: packagesDir.path); + final String packageName = p.basename(subdir.path); + final String basenamePath = p.basename(entity.path); + if (!excludedPlugins.contains(basenamePath) && + !excludedPlugins.contains(packageName) && + !excludedPlugins.contains(relativePath) && + (plugins.isEmpty || + plugins.contains(relativePath) || + plugins.contains(basenamePath))) { + yield subdir; + } + } + } + } + } + } + + /// Returns the example Dart package folders of the plugins involved in this + /// command execution. + Stream getExamples() => + getPlugins().expand(getExamplesForPlugin); + + /// Returns all Dart package folders (typically, plugin + example) of the + /// plugins involved in this command execution. + Stream getPackages() async* { + await for (Directory plugin in getPlugins()) { + yield plugin; + yield* plugin + .list(recursive: true, followLinks: false) + .where(_isDartPackage) + .cast(); + } + } + + /// Returns the files contained, recursively, within the plugins + /// involved in this command execution. + Stream getFiles() { + return getPlugins().asyncExpand((Directory folder) => folder + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity is File) + .cast()); + } + + /// Returns whether the specified entity is a directory containing a + /// `pubspec.yaml` file. + bool _isDartPackage(FileSystemEntity entity) { + return entity is Directory && + fileSystem.file(p.join(entity.path, 'pubspec.yaml')).existsSync(); + } + + /// Returns the example Dart packages contained in the specified plugin, or + /// an empty List, if the plugin has no examples. + Iterable getExamplesForPlugin(Directory plugin) { + final Directory exampleFolder = + fileSystem.directory(p.join(plugin.path, 'example')); + if (!exampleFolder.existsSync()) { + return []; + } + if (isFlutterPackage(exampleFolder, fileSystem)) { + return [exampleFolder]; + } + // Only look at the subdirectories of the example directory if the example + // directory itself is not a Dart package, and only look one level below the + // example directory for other dart packages. + return exampleFolder + .listSync() + .where( + (FileSystemEntity entity) => isFlutterPackage(entity, fileSystem)) + .cast(); + } +} + +/// A class used to run processes. +/// +/// We use this instead of directly running the process so it can be overridden +/// in tests. +class ProcessRunner { + const ProcessRunner(); + + /// Run the [executable] with [args] and stream output to stderr and stdout. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// + /// Returns the exit code of the [executable]. + Future runAndStream( + String executable, + List args, { + Directory workingDir, + bool exitOnError = false, + }) async { + print( + 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDir?.path); + await io.stdout.addStream(process.stdout); + await io.stderr.addStream(process.stderr); + if (exitOnError && await process.exitCode != 0) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error See above for details.'); + throw ToolExit(await process.exitCode); + } + return process.exitCode; + } + + /// Run the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// + /// Returns the [io.ProcessResult] of the [executable]. + Future run(String executable, List args, + {Directory workingDir, + bool exitOnError = false, + stdoutEncoding = io.systemEncoding, + stderrEncoding = io.systemEncoding}) async { + return io.Process.run(executable, args, + workingDirectory: workingDir?.path, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding); + } + + /// Starts the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// Returns the started [io.Process]. + Future start(String executable, List args, + {Directory workingDirectory}) async { + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDirectory?.path); + return process; + } + + /// Run the [executable] with [args], throwing an error on non-zero exit code. + /// + /// Unlike [runAndStream], this does not stream the process output to stdout. + /// It also unconditionally throws an error on a non-zero exit code. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// Returns the [io.ProcessResult] of running the [executable]. + Future runAndExitOnError( + String executable, + List args, { + Directory workingDir, + }) async { + final io.ProcessResult result = await io.Process.run(executable, args, + workingDirectory: workingDir?.path); + if (result.exitCode != 0) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error Stderr:\n${result.stdout}'); + throw ToolExit(result.exitCode); + } + return result; + } + + String _getErrorString(String executable, List args, + {Directory workingDir}) { + final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; + return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; + } +} diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart new file mode 100644 index 000000000000..0f1431c5aee0 --- /dev/null +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -0,0 +1,200 @@ +// 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:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common.dart'; + +// TODO(cyanglaz): Add tests for this command. +// https://github.com/flutter/flutter/issues/61049 +class CreateAllPluginsAppCommand extends PluginCommand { + CreateAllPluginsAppCommand(Directory packagesDir, FileSystem fileSystem) + : super(packagesDir, fileSystem); + + @override + String get description => + 'Generate Flutter app that includes all plugins in packages.'; + + @override + String get name => 'all-plugins-app'; + + @override + Future run() async { + final int exitCode = await _createPlugin(); + if (exitCode != 0) { + throw ToolExit(exitCode); + } + + await Future.wait(>[ + _genPubspecWithAllPlugins(), + _updateAppGradle(), + _updateManifest(), + ]); + } + + Future _createPlugin() async { + final io.ProcessResult result = io.Process.runSync( + 'flutter', + [ + 'create', + '--template=app', + '--project-name=all_plugins', + '--android-language=java', + './all_plugins', + ], + ); + + print(result.stdout); + print(result.stderr); + return result.exitCode; + } + + Future _updateAppGradle() async { + final File gradleFile = fileSystem.file(p.join( + 'all_plugins', + 'android', + 'app', + 'build.gradle', + )); + if (!gradleFile.existsSync()) { + throw ToolExit(64); + } + + final StringBuffer newGradle = StringBuffer(); + for (String line in gradleFile.readAsLinesSync()) { + newGradle.writeln(line); + if (line.contains('defaultConfig {')) { + newGradle.writeln(' multiDexEnabled true'); + } else if (line.contains('dependencies {')) { + newGradle.writeln( + ' implementation \'com.google.guava:guava:27.0.1-android\'\n', + ); + // Tests for https://github.com/flutter/flutter/issues/43383 + newGradle.writeln( + " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", + ); + } + } + gradleFile.writeAsStringSync(newGradle.toString()); + } + + Future _updateManifest() async { + final File manifestFile = fileSystem.file(p.join( + 'all_plugins', + 'android', + 'app', + 'src', + 'main', + 'AndroidManifest.xml', + )); + if (!manifestFile.existsSync()) { + throw ToolExit(64); + } + + final StringBuffer newManifest = StringBuffer(); + for (String line in manifestFile.readAsLinesSync()) { + if (line.contains('package="com.example.all_plugins"')) { + newManifest + ..writeln('package="com.example.all_plugins"') + ..writeln('xmlns:tools="http://schemas.android.com/tools">') + ..writeln() + ..writeln( + '', + ); + } else { + newManifest.writeln(line); + } + } + manifestFile.writeAsStringSync(newManifest.toString()); + } + + Future _genPubspecWithAllPlugins() async { + final Map pluginDeps = + await _getValidPathDependencies(); + final Pubspec pubspec = Pubspec( + 'all_plugins', + description: 'Flutter app containing all 1st party plugins.', + version: Version.parse('1.0.0+1'), + environment: { + 'sdk': VersionConstraint.compatibleWith( + Version.parse('2.0.0'), + ), + }, + dependencies: { + 'flutter': SdkDependency('flutter'), + }..addAll(pluginDeps), + devDependencies: { + 'flutter_test': SdkDependency('flutter'), + }, + dependencyOverrides: pluginDeps, + ); + final File pubspecFile = + fileSystem.file(p.join('all_plugins', 'pubspec.yaml')); + pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); + } + + Future> _getValidPathDependencies() async { + final Map pathDependencies = + {}; + + await for (Directory package in getPlugins()) { + final String pluginName = package.path.split('/').last; + final File pubspecFile = + fileSystem.file(p.join(package.path, 'pubspec.yaml')); + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + + if (pubspec.publishTo != 'none') { + pathDependencies[pluginName] = PathDependency(package.path); + } + } + return pathDependencies; + } + + String _pubspecToString(Pubspec pubspec) { + return ''' +### Generated file. Do not edit. Run `pub global run flutter_plugin_tools gen-pubspec` to update. +name: ${pubspec.name} +description: ${pubspec.description} + +version: ${pubspec.version} + +environment:${_pubspecMapString(pubspec.environment)} + +dependencies:${_pubspecMapString(pubspec.dependencies)} + +dependency_overrides:${_pubspecMapString(pubspec.dependencyOverrides)} + +dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} +###'''; + } + + String _pubspecMapString(Map values) { + final StringBuffer buffer = StringBuffer(); + + for (MapEntry entry in values.entries) { + buffer.writeln(); + if (entry.value is VersionConstraint) { + buffer.write(' ${entry.key}: ${entry.value}'); + } else if (entry.value is SdkDependency) { + final SdkDependency dep = entry.value; + buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); + } else if (entry.value is PathDependency) { + final PathDependency dep = entry.value; + buffer.write(' ${entry.key}: \n path: ${dep.path}'); + } else { + throw UnimplementedError( + 'Not available for type: ${entry.value.runtimeType}', + ); + } + } + + return buffer.toString(); + } +} diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart new file mode 100644 index 000000000000..0bd531a20f8a --- /dev/null +++ b/script/tool/lib/src/drive_examples_command.dart @@ -0,0 +1,209 @@ +// 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 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'common.dart'; + +class DriveExamplesCommand extends PluginCommand { + DriveExamplesCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag(kLinux, + help: 'Runs the Linux implementation of the examples'); + argParser.addFlag(kMacos, + help: 'Runs the macOS implementation of the examples'); + argParser.addFlag(kWindows, + help: 'Runs the Windows implementation of the examples'); + argParser.addFlag(kIos, + help: 'Runs the iOS implementation of the examples'); + argParser.addFlag(kAndroid, + help: 'Runs the Android implementation of the examples'); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: + 'Runs the driver tests in Dart VM with the given experiments enabled.', + ); + } + + @override + final String name = 'drive-examples'; + + @override + final String description = 'Runs driver tests for plugin example apps.\n\n' + 'For each *_test.dart in test_driver/ it drives an application with a ' + 'corresponding name in the test/ or test_driver/ directories.\n\n' + 'For example, test_driver/app_test.dart would match test/app.dart.\n\n' + 'This command requires "flutter" to be in your path.\n\n' + 'If a file with a corresponding name cannot be found, this driver file' + 'will be used to drive the tests that match ' + 'integration_test/*_test.dart.'; + + @override + Future run() async { + checkSharding(); + final List failingTests = []; + final bool isLinux = argResults[kLinux]; + final bool isMacos = argResults[kMacos]; + final bool isWindows = argResults[kWindows]; + await for (Directory plugin in getPlugins()) { + final String flutterCommand = + LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + for (Directory example in getExamplesForPlugin(plugin)) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + if (!(await pluginSupportedOnCurrentPlatform(plugin, fileSystem))) { + continue; + } + final Directory driverTests = + fileSystem.directory(p.join(example.path, 'test_driver')); + if (!driverTests.existsSync()) { + // No driver tests available for this example + continue; + } + // Look for driver tests ending in _test.dart in test_driver/ + await for (FileSystemEntity test in driverTests.list()) { + final String driverTestName = + p.relative(test.path, from: driverTests.path); + if (!driverTestName.endsWith('_test.dart')) { + continue; + } + // Try to find a matching app to drive without the _test.dart + final String deviceTestName = driverTestName.replaceAll( + RegExp(r'_test.dart$'), + '.dart', + ); + String deviceTestPath = p.join('test', deviceTestName); + if (!fileSystem + .file(p.join(example.path, deviceTestPath)) + .existsSync()) { + // If the app isn't in test/ folder, look in test_driver/ instead. + deviceTestPath = p.join('test_driver', deviceTestName); + } + + final List targetPaths = []; + if (fileSystem + .file(p.join(example.path, deviceTestPath)) + .existsSync()) { + targetPaths.add(deviceTestPath); + } else { + final Directory integrationTests = + fileSystem.directory(p.join(example.path, 'integration_test')); + + if (await integrationTests.exists()) { + await for (FileSystemEntity integration_test + in integrationTests.list()) { + if (!integration_test.basename.endsWith('_test.dart')) { + continue; + } + targetPaths + .add(p.relative(integration_test.path, from: example.path)); + } + } + + if (targetPaths.isEmpty) { + print(''' +Unable to infer a target application for $driverTestName to drive. +Tried searching for the following: +1. test/$deviceTestName +2. test_driver/$deviceTestName +3. test_driver/*_test.dart +'''); + failingTests.add(p.relative(test.path, from: example.path)); + continue; + } + } + + final List driveArgs = ['drive']; + + final String enableExperiment = argResults[kEnableExperiment]; + if (enableExperiment.isNotEmpty) { + driveArgs.add('--enable-experiment=$enableExperiment'); + } + + if (isLinux && isLinuxPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'linux', + ]); + } + if (isMacos && isMacOsPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'macos', + ]); + } + if (isWindows && isWindowsPlugin(plugin, fileSystem)) { + driveArgs.addAll([ + '-d', + 'windows', + ]); + } + + for (final targetPath in targetPaths) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + ...driveArgs, + '--driver', + p.join('test_driver', driverTestName), + '--target', + targetPath, + ], + workingDir: example, + exitOnError: true); + if (exitCode != 0) { + failingTests.add(p.join(packageName, deviceTestPath)); + } + } + } + } + } + print('\n\n'); + + if (failingTests.isNotEmpty) { + print('The following driver tests are failing (see above for details):'); + for (String test in failingTests) { + print(' * $test'); + } + throw ToolExit(1); + } + + print('All driver tests successful!'); + } + + Future pluginSupportedOnCurrentPlatform( + FileSystemEntity plugin, FileSystem fileSystem) async { + final bool isLinux = argResults[kLinux]; + final bool isMacos = argResults[kMacos]; + final bool isWindows = argResults[kWindows]; + final bool isIOS = argResults[kIos]; + final bool isAndroid = argResults[kAndroid]; + if (isLinux) { + return isLinuxPlugin(plugin, fileSystem); + } + if (isMacos) { + return isMacOsPlugin(plugin, fileSystem); + } + if (isWindows) { + return isWindowsPlugin(plugin, fileSystem); + } + if (isIOS) { + return isIosPlugin(plugin, fileSystem); + } + if (isAndroid) { + return (isAndroidPlugin(plugin, fileSystem)); + } + // When we are here, no flags are specified. Only return true if the plugin + // supports Android for legacy command support. TODO(cyanglaz): Make Android + // flag also required like other platforms (breaking change). + // https://github.com/flutter/flutter/issues/58285 + return isAndroidPlugin(plugin, fileSystem); + } +} diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart new file mode 100644 index 000000000000..0b4b2a471dbc --- /dev/null +++ b/script/tool/lib/src/firebase_test_lab_command.dart @@ -0,0 +1,264 @@ +// 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:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:uuid/uuid.dart'; + +import 'common.dart'; + +class FirebaseTestLabCommand extends PluginCommand { + FirebaseTestLabCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + Print print = print, + }) : _print = print, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + 'project', + defaultsTo: 'flutter-infra', + help: 'The Firebase project name.', + ); + argParser.addOption('service-key', + defaultsTo: + p.join(io.Platform.environment['HOME'], 'gcloud-service-key.json')); + argParser.addOption('test-run-id', + defaultsTo: Uuid().v4(), + help: + 'Optional string to append to the results path, to avoid conflicts. ' + 'Randomly chosen on each invocation if none is provided. ' + 'The default shown here is just an example.'); + argParser.addMultiOption('device', + splitCommas: false, + defaultsTo: [ + 'model=walleye,version=26', + 'model=flame,version=29' + ], + help: + 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); + argParser.addOption('results-bucket', + defaultsTo: 'gs://flutter_firebase_testlab'); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + @override + final String name = 'firebase-test-lab'; + + @override + final String description = 'Runs the instrumentation tests of the example ' + 'apps on Firebase Test Lab.\n\n' + 'Runs tests in test_instrumentation folder using the ' + 'instrumentation_test package.'; + + static const String _gradleWrapper = 'gradlew'; + + final Print _print; + + Completer _firebaseProjectConfigured; + + Future _configureFirebaseProject() async { + if (_firebaseProjectConfigured != null) { + return _firebaseProjectConfigured.future; + } else { + _firebaseProjectConfigured = Completer(); + } + await processRunner.runAndExitOnError('gcloud', [ + 'auth', + 'activate-service-account', + '--key-file=${argResults['service-key']}', + ]); + int exitCode = await processRunner.runAndStream('gcloud', [ + 'config', + 'set', + 'project', + argResults['project'], + ]); + if (exitCode == 0) { + _print('\nFirebase project configured.'); + return; + } else { + _print( + '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); + } + _firebaseProjectConfigured.complete(null); + } + + @override + Future run() async { + checkSharding(); + final Stream packagesWithTests = getPackages().where( + (Directory d) => + isFlutterPackage(d, fileSystem) && + fileSystem + .directory(p.join( + d.path, 'example', 'android', 'app', 'src', 'androidTest')) + .existsSync()); + + final List failingPackages = []; + final List missingFlutterBuild = []; + int resultsCounter = + 0; // We use a unique GCS bucket for each Firebase Test Lab run + await for (Directory package in packagesWithTests) { + // See https://github.com/flutter/flutter/issues/38983 + + final Directory exampleDirectory = + fileSystem.directory(p.join(package.path, 'example')); + final String packageName = + p.relative(package.path, from: packagesDir.path); + _print('\nRUNNING FIREBASE TEST LAB TESTS for $packageName'); + + final Directory androidDirectory = + fileSystem.directory(p.join(exampleDirectory.path, 'android')); + + final String enableExperiment = argResults[kEnableExperiment]; + final String encodedEnableExperiment = + Uri.encodeComponent('--enable-experiment=$enableExperiment'); + + // Ensures that gradle wrapper exists + if (!fileSystem + .file(p.join(androidDirectory.path, _gradleWrapper)) + .existsSync()) { + final int exitCode = await processRunner.runAndStream( + 'flutter', + [ + 'build', + 'apk', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + continue; + } + + await _configureFirebaseProject(); + + int exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + [ + 'app:assembleAndroidTest', + '-Pverbose=true', + if (enableExperiment.isNotEmpty) + '-Pextra-front-end-options=$encodedEnableExperiment', + if (enableExperiment.isNotEmpty) + '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + + // Look for tests recursively in folders that start with 'test' and that + // live in the root or example folders. + bool isTestDir(FileSystemEntity dir) { + return p.basename(dir.path).startsWith('test') || + p.basename(dir.path) == 'integration_test'; + } + + final List testDirs = + package.listSync().where(isTestDir).toList(); + final Directory example = + fileSystem.directory(p.join(package.path, 'example')); + testDirs.addAll(example.listSync().where(isTestDir).toList()); + for (Directory testDir in testDirs) { + bool isE2ETest(FileSystemEntity file) { + return file.path.endsWith('_e2e.dart') || + (file.parent.basename == 'integration_test' && + file.path.endsWith('_test.dart')); + } + + final List testFiles = testDir + .listSync(recursive: true, followLinks: true) + .where(isE2ETest) + .toList(); + for (FileSystemEntity test in testFiles) { + exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + [ + 'app:assembleDebug', + '-Pverbose=true', + '-Ptarget=${test.path}', + if (enableExperiment.isNotEmpty) + '-Pextra-front-end-options=$encodedEnableExperiment', + if (enableExperiment.isNotEmpty) + '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + final String buildId = io.Platform.environment['CIRRUS_BUILD_ID']; + final String testRunId = argResults['test-run-id']; + final String resultsDir = + 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; + final List args = [ + 'firebase', + 'test', + 'android', + 'run', + '--type', + 'instrumentation', + '--app', + 'build/app/outputs/apk/debug/app-debug.apk', + '--test', + 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + '--timeout', + '5m', + '--results-bucket=${argResults['results-bucket']}', + '--results-dir=${resultsDir}', + ]; + for (String device in argResults['device']) { + args.addAll(['--device', device]); + } + exitCode = await processRunner.runAndStream('gcloud', args, + workingDir: exampleDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + } + } + } + + _print('\n\n'); + if (failingPackages.isNotEmpty) { + _print( + 'The instrumentation tests for the following packages are failing (see above for' + 'details):'); + for (String package in failingPackages) { + _print(' * $package'); + } + } + if (missingFlutterBuild.isNotEmpty) { + _print('Run "pub global run flutter_plugin_tools build-examples --apk" on' + 'the following packages before executing tests again:'); + for (String package in missingFlutterBuild) { + _print(' * $package'); + } + } + + if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { + throw ToolExit(1); + } + + _print('All Firebase Test Lab tests successful!'); + } +} diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart new file mode 100644 index 000000000000..ec326b96c1f9 --- /dev/null +++ b/script/tool/lib/src/format_command.dart @@ -0,0 +1,147 @@ +// 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:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as p; +import 'package:quiver/iterables.dart'; + +import 'common.dart'; + +const String _googleFormatterUrl = + 'https://github.com/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'; + +class FormatCommand extends PluginCommand { + FormatCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addFlag('travis', hide: true); + argParser.addOption('clang-format', + defaultsTo: 'clang-format', + help: 'Path to executable of clang-format v5.'); + } + + @override + final String name = 'format'; + + @override + final String description = + 'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n' + 'This command requires "git", "flutter" and "clang-format" v5 to be in ' + 'your path.'; + + @override + Future run() async { + checkSharding(); + final String googleFormatterPath = await _getGoogleFormatterPath(); + + await _formatDart(); + await _formatJava(googleFormatterPath); + await _formatCppAndObjectiveC(); + + if (argResults['travis']) { + final bool modified = await _didModifyAnything(); + if (modified) { + throw ToolExit(1); + } + } + } + + Future _didModifyAnything() async { + final io.ProcessResult modifiedFiles = await processRunner + .runAndExitOnError('git', ['ls-files', '--modified'], + workingDir: packagesDir); + + print('\n\n'); + + if (modifiedFiles.stdout.isEmpty) { + print('All files formatted correctly.'); + return false; + } + + print('These files are not formatted correctly (see diff below):'); + LineSplitter.split(modifiedFiles.stdout) + .map((String line) => ' $line') + .forEach(print); + + print('\nTo fix run "pub global activate flutter_plugin_tools && ' + 'pub global run flutter_plugin_tools format" or copy-paste ' + 'this command into your terminal:'); + + print('patch -p1 <['diff'], workingDir: packagesDir); + print(diff.stdout); + print('DONE'); + return true; + } + + Future _formatCppAndObjectiveC() async { + print('Formatting all .cc, .cpp, .mm, .m, and .h files...'); + final Iterable allFiles = [] + ..addAll(await _getFilesWithExtension('.h')) + ..addAll(await _getFilesWithExtension('.m')) + ..addAll(await _getFilesWithExtension('.mm')) + ..addAll(await _getFilesWithExtension('.cc')) + ..addAll(await _getFilesWithExtension('.cpp')); + // Split this into multiple invocations to avoid a + // 'ProcessException: Argument list too long'. + final Iterable> batches = partition(allFiles, 100); + for (List batch in batches) { + await processRunner.runAndStream(argResults['clang-format'], + ['-i', '--style=Google']..addAll(batch), + workingDir: packagesDir, exitOnError: true); + } + } + + Future _formatJava(String googleFormatterPath) async { + print('Formatting all .java files...'); + final Iterable javaFiles = await _getFilesWithExtension('.java'); + await processRunner.runAndStream('java', + ['-jar', googleFormatterPath, '--replace']..addAll(javaFiles), + workingDir: packagesDir, exitOnError: true); + } + + Future _formatDart() async { + // This actually should be fine for non-Flutter Dart projects, no need to + // specifically shell out to dartfmt -w in that case. + print('Formatting all .dart files...'); + final Iterable dartFiles = await _getFilesWithExtension('.dart'); + if (dartFiles.isEmpty) { + print( + 'No .dart files to format. If you set the `--exclude` flag, most likey they were skipped'); + } else { + await processRunner.runAndStream( + 'flutter', ['format']..addAll(dartFiles), + workingDir: packagesDir, exitOnError: true); + } + } + + Future> _getFilesWithExtension(String extension) async => + getFiles() + .where((File file) => p.extension(file.path) == extension) + .map((File file) => file.path) + .toList(); + + Future _getGoogleFormatterPath() async { + final String javaFormatterPath = p.join( + p.dirname(p.fromUri(io.Platform.script)), + 'google-java-format-1.3-all-deps.jar'); + final File javaFormatterFile = fileSystem.file(javaFormatterPath); + + if (!javaFormatterFile.existsSync()) { + print('Downloading Google Java Format...'); + final http.Response response = await http.get(_googleFormatterUrl); + javaFormatterFile.writeAsBytesSync(response.bodyBytes); + } + + return javaFormatterPath; + } +} diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart new file mode 100644 index 000000000000..cf605bfc5ce2 --- /dev/null +++ b/script/tool/lib/src/java_test_command.dart @@ -0,0 +1,89 @@ +// 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 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class JavaTestCommand extends PluginCommand { + JavaTestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner); + + @override + final String name = 'java-test'; + + @override + final String description = 'Runs the Java tests of the example apps.\n\n' + 'Building the apks of the example apps is required before executing this' + 'command.'; + + static const String _gradleWrapper = 'gradlew'; + + @override + Future run() async { + checkSharding(); + final Stream examplesWithTests = getExamples().where( + (Directory d) => + isFlutterPackage(d, fileSystem) && + fileSystem + .directory(p.join(d.path, 'android', 'app', 'src', 'test')) + .existsSync()); + + final List failingPackages = []; + final List missingFlutterBuild = []; + await for (Directory example in examplesWithTests) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + print('\nRUNNING JAVA TESTS for $packageName'); + + final Directory androidDirectory = + fileSystem.directory(p.join(example.path, 'android')); + if (!fileSystem + .file(p.join(androidDirectory.path, _gradleWrapper)) + .existsSync()) { + print('ERROR: Run "flutter build apk" on example app of $packageName' + 'before executing tests.'); + missingFlutterBuild.add(packageName); + continue; + } + + final int exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + ['testDebugUnitTest', '--info'], + workingDir: androidDirectory); + if (exitCode != 0) { + failingPackages.add(packageName); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print( + 'The Java tests for the following packages are failing (see above for' + 'details):'); + for (String package in failingPackages) { + print(' * $package'); + } + } + if (missingFlutterBuild.isNotEmpty) { + print('Run "pub global run flutter_plugin_tools build-examples --apk" on' + 'the following packages before executing tests again:'); + for (String package in missingFlutterBuild) { + print(' * $package'); + } + } + + if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { + throw ToolExit(1); + } + + print('All Java tests successful!'); + } +} diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart new file mode 100644 index 000000000000..68fd4b61dd66 --- /dev/null +++ b/script/tool/lib/src/lint_podspecs_command.dart @@ -0,0 +1,146 @@ +// 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:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common.dart'; + +typedef void Print(Object object); + +/// Lint the CocoaPod podspecs, run the static analyzer on iOS/macOS plugin +/// platform code, and run unit tests. +/// +/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +class LintPodspecsCommand extends PluginCommand { + LintPodspecsCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + Print print = print, + }) : _print = print, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addMultiOption('skip', + help: + 'Skip all linting for podspecs with this basename (example: federated plugins with placeholder podspecs)', + valueHelp: 'podspec_file_name'); + argParser.addMultiOption('ignore-warnings', + help: + 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs with this basename (example: plugins with known warnings)', + valueHelp: 'podspec_file_name'); + argParser.addMultiOption('no-analyze', + help: + 'Do not pass --analyze flag to "pod lib lint" for podspecs with this basename (example: plugins with known analyzer warnings)', + valueHelp: 'podspec_file_name'); + } + + @override + final String name = 'podspecs'; + + @override + List get aliases => ['podspec']; + + @override + final String description = + 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' + 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; + + final Platform platform; + + final Print _print; + + @override + Future run() async { + if (!platform.isMacOS) { + _print('Detected platform is not macOS, skipping podspec lint'); + return; + } + + checkSharding(); + + await processRunner.runAndExitOnError('which', ['pod'], + workingDir: packagesDir); + + _print('Starting podspec lint test'); + + final List failingPlugins = []; + for (File podspec in await _podspecsToLint()) { + if (!await _lintPodspec(podspec)) { + failingPlugins.add(p.basenameWithoutExtension(podspec.path)); + } + } + + _print('\n\n'); + if (failingPlugins.isNotEmpty) { + _print('The following plugins have podspec errors (see above):'); + failingPlugins.forEach((String plugin) { + _print(' * $plugin'); + }); + throw ToolExit(1); + } + } + + Future> _podspecsToLint() async { + final List podspecs = await getFiles().where((File entity) { + final String filePath = entity.path; + return p.extension(filePath) == '.podspec' && + !argResults['skip'].contains(p.basenameWithoutExtension(filePath)); + }).toList(); + + podspecs.sort( + (File a, File b) => p.basename(a.path).compareTo(p.basename(b.path))); + return podspecs; + } + + Future _lintPodspec(File podspec) async { + // Do not run the static analyzer on plugins with known analyzer issues. + final String podspecPath = podspec.path; + final bool runAnalyzer = !argResults['no-analyze'] + .contains(p.basenameWithoutExtension(podspecPath)); + + final String podspecBasename = p.basename(podspecPath); + if (runAnalyzer) { + _print('Linting and analyzing $podspecBasename'); + } else { + _print('Linting $podspecBasename'); + } + + // Lint plugin as framework (use_frameworks!). + final ProcessResult frameworkResult = await _runPodLint(podspecPath, + runAnalyzer: runAnalyzer, libraryLint: true); + _print(frameworkResult.stdout); + _print(frameworkResult.stderr); + + // Lint plugin as library. + final ProcessResult libraryResult = await _runPodLint(podspecPath, + runAnalyzer: runAnalyzer, libraryLint: false); + _print(libraryResult.stdout); + _print(libraryResult.stderr); + + return frameworkResult.exitCode == 0 && libraryResult.exitCode == 0; + } + + Future _runPodLint(String podspecPath, + {bool runAnalyzer, bool libraryLint}) async { + final bool allowWarnings = argResults['ignore-warnings'] + .contains(p.basenameWithoutExtension(podspecPath)); + final List arguments = [ + 'lib', + 'lint', + podspecPath, + if (allowWarnings) '--allow-warnings', + if (runAnalyzer) '--analyze', + if (libraryLint) '--use-libraries' + ]; + + return processRunner.run('pod', arguments, + workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); + } +} diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart new file mode 100644 index 000000000000..7f94daac7096 --- /dev/null +++ b/script/tool/lib/src/list_command.dart @@ -0,0 +1,60 @@ +// 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 'package:file/file.dart'; + +import 'common.dart'; + +class ListCommand extends PluginCommand { + ListCommand(Directory packagesDir, FileSystem fileSystem) + : super(packagesDir, fileSystem) { + argParser.addOption( + _type, + defaultsTo: _plugin, + allowed: [_plugin, _example, _package, _file], + help: 'What type of file system content to list.', + ); + } + + static const String _type = 'type'; + static const String _plugin = 'plugin'; + static const String _example = 'example'; + static const String _package = 'package'; + static const String _file = 'file'; + + @override + final String name = 'list'; + + @override + final String description = 'Lists packages or files'; + + @override + Future run() async { + checkSharding(); + switch (argResults[_type]) { + case _plugin: + await for (Directory package in getPlugins()) { + print(package.path); + } + break; + case _example: + await for (Directory package in getExamples()) { + print(package.path); + } + break; + case _package: + await for (Directory package in getPackages()) { + print(package.path); + } + break; + case _file: + await for (File file in getFiles()) { + print(file.path); + } + break; + } + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart new file mode 100644 index 000000000000..fa81597237d7 --- /dev/null +++ b/script/tool/lib/src/main.dart @@ -0,0 +1,65 @@ +// 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:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/publish_check_command.dart'; +import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; +import 'package:path/path.dart' as p; + +import 'analyze_command.dart'; +import 'build_examples_command.dart'; +import 'common.dart'; +import 'create_all_plugins_app_command.dart'; +import 'drive_examples_command.dart'; +import 'firebase_test_lab_command.dart'; +import 'format_command.dart'; +import 'java_test_command.dart'; +import 'lint_podspecs_command.dart'; +import 'list_command.dart'; +import 'test_command.dart'; +import 'version_check_command.dart'; +import 'xctest_command.dart'; + +void main(List args) { + final FileSystem fileSystem = const LocalFileSystem(); + + Directory packagesDir = fileSystem + .directory(p.join(fileSystem.currentDirectory.path, 'packages')); + + if (!packagesDir.existsSync()) { + if (p.basename(fileSystem.currentDirectory.path) == 'packages') { + packagesDir = fileSystem.currentDirectory; + } else { + print('Error: Cannot find a "packages" sub-directory'); + io.exit(1); + } + } + + final CommandRunner commandRunner = CommandRunner( + 'pub global run flutter_plugin_tools', + 'Productivity utils for hosting multiple plugins within one repository.') + ..addCommand(AnalyzeCommand(packagesDir, fileSystem)) + ..addCommand(BuildExamplesCommand(packagesDir, fileSystem)) + ..addCommand(CreateAllPluginsAppCommand(packagesDir, fileSystem)) + ..addCommand(DriveExamplesCommand(packagesDir, fileSystem)) + ..addCommand(FirebaseTestLabCommand(packagesDir, fileSystem)) + ..addCommand(FormatCommand(packagesDir, fileSystem)) + ..addCommand(JavaTestCommand(packagesDir, fileSystem)) + ..addCommand(LintPodspecsCommand(packagesDir, fileSystem)) + ..addCommand(ListCommand(packagesDir, fileSystem)) + ..addCommand(PublishCheckCommand(packagesDir, fileSystem)) + ..addCommand(PublishPluginCommand(packagesDir, fileSystem)) + ..addCommand(TestCommand(packagesDir, fileSystem)) + ..addCommand(VersionCheckCommand(packagesDir, fileSystem)) + ..addCommand(XCTestCommand(packagesDir, fileSystem)); + + commandRunner.run(args).catchError((Object e) { + final ToolExit toolExit = e; + io.exit(toolExit.exitCode); + }, test: (Object e) => e is ToolExit); +} diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart new file mode 100644 index 000000000000..670fedaf2fa1 --- /dev/null +++ b/script/tool/lib/src/publish_check_command.dart @@ -0,0 +1,125 @@ +// 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:io' as io; + +import 'package:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common.dart'; + +class PublishCheckCommand extends PluginCommand { + PublishCheckCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner); + + @override + final String name = 'publish-check'; + + @override + final String description = + 'Checks to make sure that a plugin *could* be published.'; + + @override + Future run() async { + checkSharding(); + final List failedPackages = []; + + await for (Directory plugin in getPlugins()) { + if (!(await passesPublishCheck(plugin))) failedPackages.add(plugin); + } + + if (failedPackages.isNotEmpty) { + final String error = + 'FAIL: The following ${failedPackages.length} package(s) failed the ' + 'publishing check:'; + final String joinedFailedPackages = failedPackages.join('\n'); + + final Colorize colorizedError = Colorize('$error\n$joinedFailedPackages') + ..red(); + print(colorizedError); + throw ToolExit(1); + } + + final Colorize passedMessage = + Colorize('All packages passed publish check!')..green(); + print(passedMessage); + } + + Pubspec tryParsePubspec(Directory package) { + final File pubspecFile = package.childFile('pubspec.yaml'); + + try { + return Pubspec.parse(pubspecFile.readAsStringSync()); + } on Exception catch (exception) { + print( + 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}', + ); + return null; + } + } + + Future hasValidPublishCheckRun(Directory package) async { + final io.Process process = await io.Process.start( + 'flutter', + ['pub', 'publish', '--', '--dry-run'], + workingDirectory: package.path, + ); + + final StringBuffer outputBuffer = StringBuffer(); + + final Completer stdOutCompleter = Completer(); + process.stdout.listen( + (List event) { + io.stdout.add(event); + outputBuffer.write(String.fromCharCodes(event)); + }, + onDone: () => stdOutCompleter.complete(), + ); + + final Completer stdInCompleter = Completer(); + process.stderr.listen( + (List event) { + io.stderr.add(event); + outputBuffer.write(String.fromCharCodes(event)); + }, + onDone: () => stdInCompleter.complete(), + ); + + if (await process.exitCode == 0) return true; + + await stdOutCompleter.future; + await stdInCompleter.future; + + final String output = outputBuffer.toString(); + return output.contains('Package has 1 warning') && + output.contains( + 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'); + } + + Future passesPublishCheck(Directory package) async { + final String packageName = package.basename; + print('Checking that $packageName can be published.'); + + final Pubspec pubspec = tryParsePubspec(package); + if (pubspec == null) { + return false; + } else if (pubspec.publishTo == 'none') { + print('Package $packageName is marked as unpublishable. Skipping.'); + return true; + } + + if (await hasValidPublishCheckRun(package)) { + print("Package $packageName is able to be published."); + return true; + } else { + print('Unable to publish $packageName'); + return false; + } + } +} diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart new file mode 100644 index 000000000000..f7e3b5deeecf --- /dev/null +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -0,0 +1,223 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; + +import 'common.dart'; + +/// Wraps pub publish with a few niceties used by the flutter/plugin team. +/// +/// 1. Checks for any modified files in git and refuses to publish if there's an +/// issue. +/// 2. Tags the release with the format -v. +/// 3. Pushes the release to a remote. +/// +/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full +/// usage information. +/// +/// [processRunner], [print], and [stdin] can be overriden for easier testing. +class PublishPluginCommand extends PluginCommand { + PublishPluginCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + Print print = print, + Stdin stdinput, + }) : _print = print, + _stdin = stdinput ?? stdin, + super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + _packageOption, + help: 'The package to publish.' + 'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.', + ); + argParser.addMultiOption(_pubFlagsOption, + help: + 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); + argParser.addFlag( + _tagReleaseOption, + help: 'Whether or not to tag the release.', + defaultsTo: true, + negatable: true, + ); + argParser.addFlag( + _pushTagsOption, + help: + 'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.', + defaultsTo: true, + negatable: true, + ); + argParser.addOption( + _remoteOption, + help: + 'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.', + // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. + defaultsTo: 'upstream', + ); + } + + static const String _packageOption = 'package'; + static const String _tagReleaseOption = 'tag-release'; + static const String _pushTagsOption = 'push-tags'; + static const String _pubFlagsOption = 'pub-publish-flags'; + static const String _remoteOption = 'remote'; + + // Version tags should follow -v. For example, + // `flutter_plugin_tools-v0.0.24`. + static const String _tagFormat = '%PACKAGE%-v%VERSION%'; + + @override + final String name = 'publish-plugin'; + + @override + final String description = + 'Attempts to publish the given plugin and tag its release on GitHub.'; + + final Print _print; + final Stdin _stdin; + // The directory of the actual package that we are publishing. + Directory _packageDir; + StreamSubscription _stdinSubscription; + + @override + Future run() async { + checkSharding(); + _print('Checking local repo...'); + _packageDir = _checkPackageDir(); + await _checkGitStatus(); + final bool shouldPushTag = argResults[_pushTagsOption]; + final String remote = argResults[_remoteOption]; + String remoteUrl; + if (shouldPushTag) { + remoteUrl = await _verifyRemote(remote); + } + _print('Local repo is ready!'); + + await _publish(); + _print('Package published!'); + if (!argResults[_tagReleaseOption]) { + return await _finishSuccesfully(); + } + + _print('Tagging release...'); + final String tag = _getTag(); + await processRunner.runAndExitOnError('git', ['tag', tag], + workingDir: _packageDir); + if (!shouldPushTag) { + return await _finishSuccesfully(); + } + + _print('Pushing tag to $remote...'); + await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl); + await _finishSuccesfully(); + } + + Future _finishSuccesfully() async { + await _stdinSubscription.cancel(); + _print('Done!'); + } + + Directory _checkPackageDir() { + final String package = argResults[_packageOption]; + if (package == null) { + _print( + 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); + throw ToolExit(1); + } + final Directory _packageDir = packagesDir.childDirectory(package); + if (!_packageDir.existsSync()) { + _print('${_packageDir.absolute.path} does not exist.'); + throw ToolExit(1); + } + return _packageDir; + } + + Future _checkGitStatus() async { + if (!await GitDir.isGitDir(packagesDir.path)) { + _print('$packagesDir is not a valid Git repository.'); + throw ToolExit(1); + } + + final ProcessResult statusResult = await processRunner.runAndExitOnError( + 'git', + [ + 'status', + '--porcelain', + '--ignored', + _packageDir.absolute.path + ], + workingDir: _packageDir); + final String statusOutput = statusResult.stdout; + if (statusOutput.isNotEmpty) { + _print( + "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" + '$statusOutput\n' + 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); + throw ToolExit(1); + } + } + + Future _verifyRemote(String remote) async { + final ProcessResult remoteInfo = await processRunner.runAndExitOnError( + 'git', ['remote', 'get-url', remote], + workingDir: _packageDir); + return remoteInfo.stdout; + } + + Future _publish() async { + final List publishFlags = argResults[_pubFlagsOption]; + _print( + 'Running `pub publish ${publishFlags.join(' ')}` in ${_packageDir.absolute.path}...\n'); + final Process publish = await processRunner.start( + 'flutter', ['pub', 'publish'] + publishFlags, + workingDirectory: _packageDir); + publish.stdout + .transform(utf8.decoder) + .listen((String data) => _print(data)); + publish.stderr + .transform(utf8.decoder) + .listen((String data) => _print(data)); + _stdinSubscription = _stdin + .transform(utf8.decoder) + .listen((String data) => publish.stdin.writeln(data)); + final int result = await publish.exitCode; + if (result != 0) { + _print('Publish failed. Exiting.'); + throw ToolExit(result); + } + } + + String _getTag() { + final File pubspecFile = + fileSystem.file(p.join(_packageDir.path, 'pubspec.yaml')); + final YamlMap pubspecYaml = loadYaml(pubspecFile.readAsStringSync()); + final String name = pubspecYaml['name']; + final String version = pubspecYaml['version']; + // We should have failed to publish if these were unset. + assert(name.isNotEmpty && version.isNotEmpty); + return _tagFormat + .replaceAll('%PACKAGE%', name) + .replaceAll('%VERSION%', version); + } + + Future _pushTagToRemote( + {@required String remote, + @required String tag, + @required String remoteUrl}) async { + assert(remote != null && tag != null && remoteUrl != null); + _print('Ready to push $tag to $remoteUrl (y/n)?'); + final String input = _stdin.readLineSync(); + if (input.toLowerCase() != 'y') { + _print('Tag push canceled.'); + throw ToolExit(1); + } + + await processRunner.runAndExitOnError('git', ['push', remote, tag], + workingDir: packagesDir); + } +} diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart new file mode 100644 index 000000000000..e938168cfa89 --- /dev/null +++ b/script/tool/lib/src/test_command.dart @@ -0,0 +1,101 @@ +// 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 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +class TestCommand extends PluginCommand { + TestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Runs the tests in Dart VM with the given experiments enabled.', + ); + } + + @override + final String name = 'test'; + + @override + final String description = 'Runs the Dart tests for all packages.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + final List failingPackages = []; + await for (Directory packageDir in getPackages()) { + final String packageName = + p.relative(packageDir.path, from: packagesDir.path); + if (!fileSystem.directory(p.join(packageDir.path, 'test')).existsSync()) { + print('SKIPPING $packageName - no test subdirectory'); + continue; + } + + print('RUNNING $packageName tests...'); + + final String enableExperiment = argResults[kEnableExperiment]; + + // `flutter test` automatically gets packages. `pub run test` does not. :( + int exitCode = 0; + if (isFlutterPackage(packageDir, fileSystem)) { + final List args = [ + 'test', + '--color', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ]; + + if (isWebPlugin(packageDir, fileSystem)) { + args.add('--platform=chrome'); + } + exitCode = await processRunner.runAndStream( + 'flutter', + args, + workingDir: packageDir, + ); + } else { + exitCode = await processRunner.runAndStream( + 'pub', + ['get'], + workingDir: packageDir, + ); + if (exitCode == 0) { + exitCode = await processRunner.runAndStream( + 'pub', + [ + 'run', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + 'test', + ], + workingDir: packageDir, + ); + } + } + if (exitCode != 0) { + failingPackages.add(packageName); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print('Tests for the following packages are failing (see above):'); + failingPackages.forEach((String package) { + print(' * $package'); + }); + throw ToolExit(1); + } + + print('All tests are passing!'); + } +} diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart new file mode 100644 index 000000000000..2c6b92bbcb7a --- /dev/null +++ b/script/tool/lib/src/version_check_command.dart @@ -0,0 +1,220 @@ +// 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:io' as io; + +import 'package:meta/meta.dart'; +import 'package:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:yaml/yaml.dart'; + +import 'common.dart'; + +const String _kBaseSha = 'base_sha'; + +class GitVersionFinder { + GitVersionFinder(this.baseGitDir, this.baseSha); + + final GitDir baseGitDir; + final String baseSha; + + static bool isPubspec(String file) { + return file.trim().endsWith('pubspec.yaml'); + } + + Future> getChangedPubSpecs() async { + final io.ProcessResult changedFilesCommand = await baseGitDir + .runCommand(['diff', '--name-only', '$baseSha', 'HEAD']); + final List changedFiles = + changedFilesCommand.stdout.toString().split('\n'); + return changedFiles.where(isPubspec).toList(); + } + + Future getPackageVersion(String pubspecPath, String gitRef) async { + final io.ProcessResult gitShow = + await baseGitDir.runCommand(['show', '$gitRef:$pubspecPath']); + final String fileContent = gitShow.stdout; + final String versionString = loadYaml(fileContent)['version']; + return versionString == null ? null : Version.parse(versionString); + } +} + +enum NextVersionType { + BREAKING_MAJOR, + MAJOR_NULLSAFETY_PRE_RELEASE, + MINOR_NULLSAFETY_PRE_RELEASE, + MINOR, + PATCH, + RELEASE, +} + +Version getNextNullSafetyPreRelease(Version current, Version next) { + String nextNullsafetyPrerelease = 'nullsafety'; + if (current.isPreRelease && + current.preRelease.first is String && + current.preRelease.first == 'nullsafety') { + if (current.preRelease.length == 1) { + nextNullsafetyPrerelease = 'nullsafety.1'; + } else if (current.preRelease.length == 2 && + current.preRelease.last is int) { + nextNullsafetyPrerelease = 'nullsafety.${current.preRelease.last + 1}'; + } + } + return Version( + next.major, + next.minor, + next.patch, + pre: nextNullsafetyPrerelease, + ); +} + +@visibleForTesting +Map getAllowedNextVersions( + Version masterVersion, Version headVersion) { + final Version nextNullSafetyMajor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMajor); + final Version nextNullSafetyMinor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); + final Map allowedNextVersions = + { + masterVersion.nextMajor: NextVersionType.BREAKING_MAJOR, + nextNullSafetyMajor: NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE, + nextNullSafetyMinor: NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE, + masterVersion.nextMinor: NextVersionType.MINOR, + masterVersion.nextPatch: NextVersionType.PATCH, + }; + + if (masterVersion.major < 1 && headVersion.major < 1) { + int nextBuildNumber = -1; + if (masterVersion.build.isEmpty) { + nextBuildNumber = 1; + } else { + final int currentBuildNumber = masterVersion.build.first; + nextBuildNumber = currentBuildNumber + 1; + } + final Version preReleaseVersion = Version( + masterVersion.major, + masterVersion.minor, + masterVersion.patch, + build: nextBuildNumber.toString(), + ); + allowedNextVersions.clear(); + allowedNextVersions[masterVersion.nextMajor] = NextVersionType.RELEASE; + allowedNextVersions[masterVersion.nextMinor] = + NextVersionType.BREAKING_MAJOR; + allowedNextVersions[masterVersion.nextPatch] = NextVersionType.MINOR; + allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; + + final Version nextNullSafetyMajor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextMinor); + final Version nextNullSafetyMinor = + getNextNullSafetyPreRelease(masterVersion, masterVersion.nextPatch); + + allowedNextVersions[nextNullSafetyMajor] = + NextVersionType.MAJOR_NULLSAFETY_PRE_RELEASE; + allowedNextVersions[nextNullSafetyMinor] = + NextVersionType.MINOR_NULLSAFETY_PRE_RELEASE; + } + return allowedNextVersions; +} + +class VersionCheckCommand extends PluginCommand { + VersionCheckCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + this.gitDir, + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption(_kBaseSha); + } + + /// The git directory to use. By default it uses the parent directory. + /// + /// This can be mocked for testing. + final GitDir gitDir; + + @override + final String name = 'version-check'; + + @override + final String description = + 'Checks if the versions of the plugins have been incremented per pub specification.\n\n' + 'This command requires "pub" and "flutter" to be in your path.'; + + @override + Future run() async { + checkSharding(); + + final String rootDir = packagesDir.parent.absolute.path; + final String baseSha = argResults[_kBaseSha]; + + GitDir baseGitDir = gitDir; + if (baseGitDir == null) { + if (!await GitDir.isGitDir(rootDir)) { + print('$rootDir is not a valid Git repository.'); + throw ToolExit(2); + } + baseGitDir = await GitDir.fromExisting(rootDir); + } + + final GitVersionFinder gitVersionFinder = + GitVersionFinder(baseGitDir, baseSha); + + final List changedPubspecs = + await gitVersionFinder.getChangedPubSpecs(); + + for (final String pubspecPath in changedPubspecs) { + try { + final File pubspecFile = fileSystem.file(pubspecPath); + if (!pubspecFile.existsSync()) { + continue; + } + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec.publishTo == 'none') { + continue; + } + + final Version masterVersion = + await gitVersionFinder.getPackageVersion(pubspecPath, baseSha); + final Version headVersion = + await gitVersionFinder.getPackageVersion(pubspecPath, 'HEAD'); + if (headVersion == null) { + continue; // Example apps don't have versions + } + + final Map allowedNextVersions = + getAllowedNextVersions(masterVersion, headVersion); + + if (!allowedNextVersions.containsKey(headVersion)) { + final String error = '$pubspecPath incorrectly updated version.\n' + 'HEAD: $headVersion, master: $masterVersion.\n' + 'Allowed versions: $allowedNextVersions'; + final Colorize redError = Colorize(error)..red(); + print(redError); + throw ToolExit(1); + } + + bool isPlatformInterface = pubspec.name.endsWith("_platform_interface"); + if (isPlatformInterface && + allowedNextVersions[headVersion] == + NextVersionType.BREAKING_MAJOR) { + final String error = '$pubspecPath breaking change detected.\n' + 'Breaking changes to platform interfaces are strongly discouraged.\n'; + final Colorize redError = Colorize(error)..red(); + print(redError); + throw ToolExit(1); + } + } on io.ProcessException { + print('Unable to find pubspec in master for $pubspecPath.' + ' Safe to ignore if the project is new.'); + } + } + + print('No version check errors found!'); + } +} diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart new file mode 100644 index 000000000000..d90b7a8fbfea --- /dev/null +++ b/script/tool/lib/src/xctest_command.dart @@ -0,0 +1,216 @@ +// 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:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +const String _kiOSDestination = 'ios-destination'; +const String _kTarget = 'target'; +const String _kSkip = 'skip'; +const String _kXcodeBuildCommand = 'xcodebuild'; +const String _kXCRunCommand = 'xcrun'; +const String _kFoundNoSimulatorsMessage = + 'Cannot find any available simulators, tests failed'; + +/// The command to run iOS' XCTests in plugins, this should work for both XCUnitTest and XCUITest targets. +/// The tests target have to be added to the xcode project of the example app. Usually at "example/ios/Runner.xcodeproj". +/// The command takes a "-target" argument which has to match the target of the test target. +/// For information on how to add test target in an xcode project, see https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/UnitTesting.html +class XCTestCommand extends PluginCommand { + XCTestCommand( + Directory packagesDir, + FileSystem fileSystem, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, fileSystem, processRunner: processRunner) { + argParser.addOption( + _kiOSDestination, + help: + 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' + 'this is passed to the `-destination` argument in xcodebuild command.\n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', + ); + argParser.addOption(_kTarget, + help: 'The test target.\n' + 'This is the xcode project test target. This is passed to the `-scheme` argument in the xcodebuild command. \n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the scheme'); + argParser.addMultiOption(_kSkip, + help: 'Plugins to skip while running this command. \n'); + } + + @override + final String name = 'xctest'; + + @override + final String description = 'Runs the xctests in the iOS example apps.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + if (argResults[_kTarget] == null) { + // TODO(cyanglaz): Automatically find all the available testing schemes if this argument is not specified. + // https://github.com/flutter/flutter/issues/68419 + print('--$_kTarget must be specified'); + throw ToolExit(1); + } + + String destination = argResults[_kiOSDestination]; + if (destination == null) { + String simulatorId = await _findAvailableIphoneSimulator(); + if (simulatorId == null) { + print(_kFoundNoSimulatorsMessage); + throw ToolExit(1); + } + destination = 'id=$simulatorId'; + } + + checkSharding(); + + final String target = argResults[_kTarget]; + final List skipped = argResults[_kSkip]; + + List failingPackages = []; + await for (Directory plugin in getPlugins()) { + // Start running for package. + final String packageName = + p.relative(plugin.path, from: packagesDir.path); + print('Start running for $packageName ...'); + if (!isIosPlugin(plugin, fileSystem)) { + print('iOS is not supported by this plugin.'); + print('\n\n'); + continue; + } + if (skipped.contains(packageName)) { + print('$packageName was skipped with the --skip flag.'); + print('\n\n'); + continue; + } + for (Directory example in getExamplesForPlugin(plugin)) { + // Look for the test scheme in the example app. + print('Look for target named: $_kTarget ...'); + final List findSchemeArgs = [ + '-project', + 'ios/Runner.xcodeproj', + '-list', + '-json' + ]; + final String completeFindSchemeCommand = + '$_kXcodeBuildCommand ${findSchemeArgs.join(' ')}'; + print(completeFindSchemeCommand); + final io.ProcessResult xcodeprojListResult = await processRunner + .run(_kXcodeBuildCommand, findSchemeArgs, workingDir: example); + if (xcodeprojListResult.exitCode != 0) { + print('Error occurred while running "$completeFindSchemeCommand":\n' + '${xcodeprojListResult.stderr}'); + failingPackages.add(packageName); + print('\n\n'); + continue; + } + + final String xcodeprojListOutput = xcodeprojListResult.stdout; + Map xcodeprojListOutputJson = + jsonDecode(xcodeprojListOutput); + if (!xcodeprojListOutputJson['project']['targets'].contains(target)) { + failingPackages.add(packageName); + print('$target not configured for $packageName, test failed.'); + print( + 'Please check the scheme for the test target if it matches the name $target.\n' + 'If this plugin does not have an XCTest target, use the $_kSkip flag in the $name command to skip the plugin.'); + print('\n\n'); + continue; + } + // Found the scheme, running tests + print('Running XCTests:$target for $packageName ...'); + final List xctestArgs = [ + 'test', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + target, + '-destination', + destination, + 'CODE_SIGN_IDENTITY=""', + 'CODE_SIGNING_REQUIRED=NO' + ]; + final String completeTestCommand = + '$_kXcodeBuildCommand ${xctestArgs.join(' ')}'; + print(completeTestCommand); + final int exitCode = await processRunner + .runAndStream(_kXcodeBuildCommand, xctestArgs, workingDir: example); + if (exitCode == 0) { + print('Successfully ran xctest for $packageName'); + } else { + failingPackages.add(packageName); + } + } + } + + // Command end, print reports. + if (failingPackages.isEmpty) { + print("All XCTests have passed!"); + } else { + print( + 'The following packages are failing XCTests (see above for details):'); + for (String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + } + + Future _findAvailableIphoneSimulator() async { + // Find the first available destination if not specified. + final List findSimulatorsArguments = [ + 'simctl', + 'list', + '--json' + ]; + final String findSimulatorCompleteCommand = + '$_kXCRunCommand ${findSimulatorsArguments.join(' ')}'; + print('Looking for available simulators...'); + print(findSimulatorCompleteCommand); + final io.ProcessResult findSimulatorsResult = + await processRunner.run(_kXCRunCommand, findSimulatorsArguments); + if (findSimulatorsResult.exitCode != 0) { + print('Error occurred while running "$findSimulatorCompleteCommand":\n' + '${findSimulatorsResult.stderr}'); + throw ToolExit(1); + } + final Map simulatorListJson = + jsonDecode(findSimulatorsResult.stdout); + final List runtimes = simulatorListJson['runtimes']; + final Map devices = simulatorListJson['devices']; + if (runtimes.isEmpty || devices.isEmpty) { + return null; + } + String id; + // Looking for runtimes, trying to find one with highest OS version. + for (Map runtimeMap in runtimes.reversed) { + if (!runtimeMap['name'].contains('iOS')) { + continue; + } + final String runtimeID = runtimeMap['identifier']; + final List devicesForRuntime = devices[runtimeID]; + if (devicesForRuntime.isEmpty) { + continue; + } + // Looking for runtimes, trying to find latest version of device. + for (Map device in devicesForRuntime.reversed) { + if (device['availabilityError'] != null || + (device['isAvailable'] as bool == false)) { + continue; + } + id = device['udid']; + print('device selected: $device'); + return id; + } + } + return null; + } +} diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml new file mode 100644 index 000000000000..d9fce4ad26a7 --- /dev/null +++ b/script/tool/pubspec.yaml @@ -0,0 +1,25 @@ +name: flutter_plugin_tools +description: Productivity utils for hosting multiple plugins within one repository. +publish_to: 'none' + +dependencies: + args: "^1.4.3" + path: "^1.6.1" + http: "^0.12.1" + async: "^2.0.7" + yaml: "^2.1.15" + quiver: "^2.0.2" + pub_semver: ^1.4.2 + colorize: ^2.0.0 + git: ^1.0.0 + platform: ^2.2.0 + pubspec_parse: "^0.1.4" + test: ^1.6.4 + meta: ^1.1.7 + file: ^5.0.10 + uuid: ^2.0.4 + http_multi_server: ^2.2.0 + collection: 1.14.13 + +environment: + sdk: ">=2.3.0 <3.0.0"