diff --git a/.ci.yaml b/.ci.yaml index f88c794712ba..6cc325b985fe 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -25,6 +25,17 @@ platform_properties: ] device_type: none os: Windows + mac_arm64: + properties: + dependencies: >- + [ + {"dependency": "xcode", "version": "14a5294e"}, + {"dependency": "gems", "version": "v3.3.14"} + ] + os: Mac-12 + device_type: none + cpu: arm64 + xcode: 14a5294e # xcode 14.0 beta 5 mac_x64: properties: dependencies: >- @@ -36,7 +47,7 @@ platform_properties: device_type: none cpu: x86 xcode: 14a5294e # xcode 14.0 beta 5 - + targets: ### iOS+macOS tasks *** @@ -50,10 +61,10 @@ targets: properties: add_recipes_cq: "true" version_file: flutter_master.version - target_file: mac_lint_podspecs.yaml + target_file: macos_lint_podspecs.yaml ### macOS desktop tasks ### - # macos-platform_tests builds all the plugins on M1, so this build is run + # macos_platform_tests builds all the plugins on ARM, so this build is run # on Intel to give us build coverage of both host types. - name: Mac_x64 build_all_plugins master recipe: plugins/plugins @@ -61,7 +72,7 @@ targets: properties: add_recipes_cq: "true" version_file: flutter_master.version - target_file: mac_build_all_plugins.yaml + target_file: macos_build_all_plugins.yaml channel: master - name: Mac_x64 build_all_plugins stable @@ -70,97 +81,156 @@ targets: properties: add_recipes_cq: "true" version_file: flutter_stable.version - target_file: mac_build_all_plugins.yaml + target_file: macos_build_all_plugins.yaml + channel: stable + + - name: Mac_arm64 macos_platform_tests master + recipe: plugins/plugins + timeout: 60 + properties: + channel: master + add_recipes_cq: "true" + version_file: flutter_master.version + target_file: macos_platform_tests.yaml + + - name: Mac_arm64 macos_platform_tests stable + recipe: plugins/plugins + presubmit: false + timeout: 60 + properties: channel: stable + add_recipes_cq: "true" + version_file: flutter_stable.version + target_file: macos_platform_tests.yaml ### iOS tasks ### - # TODO(stuartmorgan): Swap this and ios-build_all_plugins once simulator - # tests are reliable on the ARM infrastructure. See discussion at - # https://github.com/flutter/plugins/pull/5693#issuecomment-1126011089 - - name: Mac_x64 ios_platform_tests_1_of_4 master + # ios_platform_tests builds all the plugins on ARM, so this build is run + # on Intel to give us build coverage of both host types. + - name: Mac_x64 ios_build_all_plugins master recipe: plugins/plugins timeout: 30 properties: + channel: master add_recipes_cq: "true" version_file: flutter_master.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 0 --shardCount 4" + target_file: ios_build_all_plugins.yaml - - name: Mac_x64 ios_platform_tests_2_of_4 master + - name: Mac_x64 ios_build_all_plugins stable recipe: plugins/plugins timeout: 30 + properties: + channel: stable + add_recipes_cq: "true" + version_file: flutter_stable.version + target_file: ios_build_all_plugins.yaml + + # TODO(stuartmorgan): Change all of the ios_platform_tests_* task timeouts + # to 60 minutes once https://github.com/flutter/flutter/issues/119750 is + # fixed. + - name: Mac_arm64 ios_platform_tests_shard_1 master - plugins + recipe: plugins/plugins + timeout: 120 properties: add_recipes_cq: "true" version_file: flutter_master.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 1 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 0 --shardCount 5" - - name: Mac_x64 ios_platform_tests_3_of_4 master + - name: Mac_arm64 ios_platform_tests_shard_2 master - plugins recipe: plugins/plugins - timeout: 30 + timeout: 120 properties: add_recipes_cq: "true" version_file: flutter_master.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 2 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 1 --shardCount 5" - - name: Mac_x64 ios_platform_tests_4_of_4 master + - name: Mac_arm64 ios_platform_tests_shard_3 master - plugins recipe: plugins/plugins - timeout: 30 + timeout: 120 + properties: + add_recipes_cq: "true" + version_file: flutter_master.version + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 2 --shardCount 5" + + - name: Mac_arm64 ios_platform_tests_shard_4 master - plugins + recipe: plugins/plugins + timeout: 120 + properties: + add_recipes_cq: "true" + version_file: flutter_master.version + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 3 --shardCount 5" + + - name: Mac_arm64 ios_platform_tests_shard_5 master - plugins + recipe: plugins/plugins + timeout: 120 properties: add_recipes_cq: "true" version_file: flutter_master.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 3 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 4 --shardCount 5" # Don't run full platform tests on both channels in pre-submit. - - name: Mac_x64 ios_platform_tests_1_of_4 stable + - name: Mac_arm64 ios_platform_tests_shard_1 stable - plugins recipe: plugins/plugins presubmit: false - timeout: 30 + timeout: 120 properties: channel: stable add_recipes_cq: "true" version_file: flutter_stable.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 0 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 0 --shardCount 5" - - name: Mac_x64 ios_platform_tests_2_of_4 stable + - name: Mac_arm64 ios_platform_tests_shard_2 stable - plugins recipe: plugins/plugins presubmit: false - timeout: 30 + timeout: 120 properties: channel: stable add_recipes_cq: "true" version_file: flutter_stable.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 1 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 1 --shardCount 5" - - name: Mac_x64 ios_platform_tests_3_of_4 stable + - name: Mac_arm64 ios_platform_tests_shard_3 stable - plugins recipe: plugins/plugins presubmit: false - timeout: 30 + timeout: 120 properties: channel: stable add_recipes_cq: "true" version_file: flutter_stable.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 2 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 2 --shardCount 5" - - name: Mac_x64 ios_platform_tests_4_of_4 stable + - name: Mac_arm64 ios_platform_tests_shard_4 stable - plugins recipe: plugins/plugins presubmit: false - timeout: 30 + timeout: 120 + properties: + channel: stable + add_recipes_cq: "true" + version_file: flutter_stable.version + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 3 --shardCount 5" + + - name: Mac_arm64 ios_platform_tests_shard_5 stable - plugins + recipe: plugins/plugins + presubmit: false + timeout: 120 properties: channel: stable add_recipes_cq: "true" version_file: flutter_stable.version - target_file: mac_ios_platform_tests.yaml - package_sharding: "--shardIndex 3 --shardCount 4" + target_file: ios_platform_tests.yaml + package_sharding: "--shardIndex 4 --shardCount 5" - name: Windows win32-platform_tests master recipe: plugins/plugins - timeout: 30 + timeout: 60 properties: add_recipes_cq: "true" target_file: windows_build_and_platform_tests.yaml @@ -173,7 +243,8 @@ targets: - name: Windows win32-platform_tests stable recipe: plugins/plugins - timeout: 30 + presubmit: false + timeout: 60 properties: add_recipes_cq: "true" target_file: windows_build_and_platform_tests.yaml @@ -189,7 +260,7 @@ targets: timeout: 30 properties: add_recipes_cq: "true" - target_file: build_all_plugins.yaml + target_file: windows_build_all_plugins.yaml channel: master version_file: flutter_master.version dependencies: > @@ -202,7 +273,7 @@ targets: timeout: 30 properties: add_recipes_cq: "true" - target_file: build_all_plugins.yaml + target_file: windows_build_all_plugins.yaml channel: stable version_file: flutter_stable.version dependencies: > @@ -210,15 +281,6 @@ targets: {"dependency": "vs_build", "version": "version:vs2019"} ] - - name: Windows plugin_tools_tests - recipe: plugins/plugins - timeout: 30 - properties: - add_recipes_cq: "true" - target_file: plugin_tools_tests.yaml - channel: master - version_file: flutter_master.version - - name: Linux ci_yaml plugins roller recipe: infra/ci_yaml timeout: 30 diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index c177696ee30a..ec9a0909f40f 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -973cff40b4022edd59dbf44475ffc569bece1ea8 +33e4d21f7c13e02a7c92c7272309afbff792a864 diff --git a/.ci/flutter_stable.version b/.ci/flutter_stable.version index f3933394e20a..542569bcfd31 100644 --- a/.ci/flutter_stable.version +++ b/.ci/flutter_stable.version @@ -1 +1 @@ -135454af32477f815a7525073027a3ff9eff1bfd +7048ed95a5ad3e43d697e0c397464193991fc230 diff --git a/.ci/scripts/build_all_plugins.sh b/.ci/scripts/build_all_plugins.sh index 89dab629fd52..c22b9832ff22 100644 --- a/.ci/scripts/build_all_plugins.sh +++ b/.ci/scripts/build_all_plugins.sh @@ -5,5 +5,6 @@ platform="$1" build_mode="$2" +shift 2 cd all_packages -flutter build "$platform" --"$build_mode" +flutter build "$platform" --"$build_mode" "$@" diff --git a/.ci/scripts/build_examples_win32.sh b/.ci/scripts/build_examples_win32.sh index bcf57a4b311f..ff30ca93eec1 100644 --- a/.ci/scripts/build_examples_win32.sh +++ b/.ci/scripts/build_examples_win32.sh @@ -3,5 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --windows \ +dart pub global run flutter_plugin_tools build-examples --windows \ --packages-for-branch --log-timing diff --git a/.ci/scripts/create_all_plugins_app.sh b/.ci/scripts/create_all_plugins_app.sh index 100e8aca804a..8c45a351bef4 100644 --- a/.ci/scripts/create_all_plugins_app.sh +++ b/.ci/scripts/create_all_plugins_app.sh @@ -3,5 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart ./script/tool/bin/flutter_plugin_tools.dart create-all-packages-app \ - --output-dir=. --exclude script/configs/exclude_all_plugins_app.yaml +dart pub global run flutter_plugin_tools create-all-packages-app \ + --output-dir=. --exclude script/configs/exclude_all_packages_app.yaml diff --git a/.ci/scripts/create_simulator.sh b/.ci/scripts/create_simulator.sh index 3d86739051f1..98bfb6573593 100644 --- a/.ci/scripts/create_simulator.sh +++ b/.ci/scripts/create_simulator.sh @@ -3,7 +3,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -device=com.apple.CoreSimulator.SimDeviceType.iPhone-11 +device=com.apple.CoreSimulator.SimDeviceType.iPhone-13 os=com.apple.CoreSimulator.SimRuntime.iOS-16-0 xcrun simctl list diff --git a/.ci/scripts/drive_examples_win32.sh b/.ci/scripts/drive_examples_win32.sh index c3e2e7bc5447..d06c192ab551 100644 --- a/.ci/scripts/drive_examples_win32.sh +++ b/.ci/scripts/drive_examples_win32.sh @@ -3,5 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart ./script/tool/bin/flutter_plugin_tools.dart drive-examples --windows \ +dart pub global run flutter_plugin_tools drive-examples --windows \ --exclude=script/configs/exclude_integration_win32.yaml --packages-for-branch --log-timing diff --git a/.ci/scripts/native_test_win32.sh b/.ci/scripts/native_test_win32.sh index 37cf54e55c5c..7bfe84022487 100644 --- a/.ci/scripts/native_test_win32.sh +++ b/.ci/scripts/native_test_win32.sh @@ -3,5 +3,5 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -dart ./script/tool/bin/flutter_plugin_tools.dart native-test --windows \ +dart pub global run flutter_plugin_tools native-test --windows \ --no-integration --packages-for-branch --log-timing diff --git a/.ci/scripts/prepare_tool.sh b/.ci/scripts/prepare_tool.sh index f93694bf1ff6..aced1517760c 100755 --- a/.ci/scripts/prepare_tool.sh +++ b/.ci/scripts/prepare_tool.sh @@ -6,5 +6,6 @@ # To set FETCH_HEAD for "git merge-base" to work git fetch origin main -cd script/tool -dart pub get +# Pinned version of the plugin tools, to avoid breakage in this repository +# when pushing updates from flutter/packages. +dart pub global activate flutter_plugin_tools 0.13.4+3 diff --git a/.ci/targets/ios_build_all_plugins.yaml b/.ci/targets/ios_build_all_plugins.yaml new file mode 100644 index 000000000000..7b5b88d9c9ff --- /dev/null +++ b/.ci/targets/ios_build_all_plugins.yaml @@ -0,0 +1,11 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: create all_plugins app + script: .ci/scripts/create_all_plugins_app.sh + - name: build all_plugins for iOS debug + script: .ci/scripts/build_all_plugins.sh + args: ["ios", "debug", "--no-codesign"] + - name: build all_plugins for iOS release + script: .ci/scripts/build_all_plugins.sh + args: ["ios", "release", "--no-codesign"] diff --git a/.ci/targets/mac_ios_platform_tests.yaml b/.ci/targets/ios_platform_tests.yaml similarity index 96% rename from .ci/targets/mac_ios_platform_tests.yaml rename to .ci/targets/ios_platform_tests.yaml index 5a00da7278a4..692b83dcb285 100644 --- a/.ci/targets/mac_ios_platform_tests.yaml +++ b/.ci/targets/ios_platform_tests.yaml @@ -15,7 +15,7 @@ tasks: args: ["xcode-analyze", "--ios", "--ios-min-version=13.0"] - name: native test script: script/tool_runner.sh - args: ["native-test", "--ios", "--ios-destination", "platform=iOS Simulator,name=iPhone 11,OS=latest"] + args: ["native-test", "--ios", "--ios-destination", "platform=iOS Simulator,name=iPhone 13,OS=latest"] - name: drive examples # `drive-examples` contains integration tests, which changes the UI of the application. # This UI change sometimes affects `xctest`. diff --git a/.ci/targets/mac_build_all_plugins.yaml b/.ci/targets/macos_build_all_plugins.yaml similarity index 77% rename from .ci/targets/mac_build_all_plugins.yaml rename to .ci/targets/macos_build_all_plugins.yaml index 4dd324e8b3f0..e6eb8ac2c315 100644 --- a/.ci/targets/mac_build_all_plugins.yaml +++ b/.ci/targets/macos_build_all_plugins.yaml @@ -3,9 +3,9 @@ tasks: script: .ci/scripts/prepare_tool.sh - name: create all_plugins app script: .ci/scripts/create_all_plugins_app.sh - - name: build all_plugins debug + - name: build all_plugins for macOS debug script: .ci/scripts/build_all_plugins.sh args: ["macos", "debug"] - - name: build all_plugins release + - name: build all_plugins for macOS release script: .ci/scripts/build_all_plugins.sh args: ["macos", "release"] diff --git a/.ci/targets/mac_lint_podspecs.yaml b/.ci/targets/macos_lint_podspecs.yaml similarity index 100% rename from .ci/targets/mac_lint_podspecs.yaml rename to .ci/targets/macos_lint_podspecs.yaml diff --git a/.ci/targets/macos_platform_tests.yaml b/.ci/targets/macos_platform_tests.yaml new file mode 100644 index 000000000000..4b2ee4eac1fe --- /dev/null +++ b/.ci/targets/macos_platform_tests.yaml @@ -0,0 +1,19 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: build examples + script: script/tool_runner.sh + args: ["build-examples", "--macos"] + - name: xcode analyze + script: script/tool_runner.sh + args: ["xcode-analyze", "--macos"] + - name: xcode analyze deprecation + # Ensure we don't accidentally introduce deprecated code. + script: script/tool_runner.sh + args: ["xcode-analyze", "--macos", "--macos-min-version=12.3"] + - name: native test + script: script/tool_runner.sh + args: ["native-test", "--macos"] + - name: drive examples + script: script/tool_runner.sh + args: ["drive-examples", "--macos", "--exclude=script/configs/exclude_integration_macos.yaml"] diff --git a/.ci/targets/plugin_tools_tests.yaml b/.ci/targets/plugin_tools_tests.yaml deleted file mode 100644 index 265e74bdd06b..000000000000 --- a/.ci/targets/plugin_tools_tests.yaml +++ /dev/null @@ -1,5 +0,0 @@ -tasks: - - name: prepare tool - script: .ci/scripts/prepare_tool.sh - - name: tool unit tests - script: .ci/scripts/plugin_tools_tests.sh diff --git a/.ci/targets/build_all_plugins.yaml b/.ci/targets/windows_build_all_plugins.yaml similarity index 76% rename from .ci/targets/build_all_plugins.yaml rename to .ci/targets/windows_build_all_plugins.yaml index 0ffbdfcce376..53d6b99e2444 100644 --- a/.ci/targets/build_all_plugins.yaml +++ b/.ci/targets/windows_build_all_plugins.yaml @@ -3,9 +3,9 @@ tasks: script: .ci/scripts/prepare_tool.sh - name: create all_plugins app script: .ci/scripts/create_all_plugins_app.sh - - name: build all_plugins debug + - name: build all_plugins for Windows debug script: .ci/scripts/build_all_plugins.sh args: ["windows", "debug"] - - name: build all_plugins release + - name: build all_plugins for Windows release script: .ci/scripts/build_all_plugins.sh args: ["windows", "release"] diff --git a/.cirrus.yml b/.cirrus.yml index 5f3c5fe8b39a..e9d513bf5d45 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,10 +1,10 @@ -gcp_credentials: ENCRYPTED[!9e38557f08108136b3625b7e62c64cc9eccc50365ffeaaa55c6be52f1d8fd6225af5badc69983ca08484274f02f34424!] +gcp_credentials: ENCRYPTED[!3a93d98d7c95a41f5033834ef30e50928fc5d81239dc632b153c2628200a8187f3811cb01ce338b1ab3b6505a7a65c37!] # Run on PRs and main branch post submit only. Don't run tests when tagging. only_if: $CIRRUS_TAG == '' && ($CIRRUS_PR != '' || $CIRRUS_BRANCH == 'main') env: CHANNEL: "master" # Default to master when not explicitly set by a task. - PLUGIN_TOOL_COMMAND: "dart ./script/tool/bin/flutter_plugin_tools.dart" + PLUGIN_TOOL_COMMAND: "dart pub global run flutter_plugin_tools" install_chrome_linux_template: &INSTALL_CHROME_LINUX env: @@ -22,21 +22,6 @@ tool_setup_template: &TOOL_SETUP_TEMPLATE tool_setup_script: - .ci/scripts/prepare_tool.sh -macos_template: &MACOS_TEMPLATE - # Only one macOS task can run in parallel without credits, so use them for - # PRs on macOS. - use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' - -macos_intel_template: &MACOS_INTEL_TEMPLATE - << : *MACOS_TEMPLATE - osx_instance: - image: big-sur-xcode-13 - -macos_arm_template: &MACOS_ARM_TEMPLATE - << : *MACOS_TEMPLATE - macos_instance: - image: ghcr.io/cirruslabs/macos-ventura-xcode:14 - flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE upgrade_flutter_script: # Channels that are part of our normal test matrix use a pinned, @@ -62,17 +47,19 @@ flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE - flutter doctor -v << : *TOOL_SETUP_TEMPLATE -build_all_plugins_app_template: &BUILD_ALL_PLUGINS_APP_TEMPLATE - create_all_plugins_app_script: - - $PLUGIN_TOOL_COMMAND create-all-packages-app --output-dir=. --exclude script/configs/exclude_all_plugins_app.yaml - build_all_plugins_debug_script: +# Ensures that the latest versions of all of the 1P packages can be used +# together. See script/configs/exclude_all_packages_app.yaml for exceptions. +build_all_packages_app_template: &BUILD_ALL_PACKAGES_APP_TEMPLATE + create_all_packages_app_script: + - $PLUGIN_TOOL_COMMAND create-all-packages-app --output-dir=. --exclude script/configs/exclude_all_packages_app.yaml + build_all_packages_debug_script: - cd all_packages - if [[ "$BUILD_ALL_ARGS" == "web" ]]; then - echo "Skipping; web does not support debug builds" - else - flutter build $BUILD_ALL_ARGS --debug - fi - build_all_plugins_release_script: + build_all_packages_release_script: - cd all_packages - flutter build $BUILD_ALL_ARGS --release @@ -90,10 +77,6 @@ task: namespace: default matrix: ### Platform-agnostic tasks ### - - name: Linux plugin_tools_tests - script: - - cd script/tool - - dart pub run test # Repository rules and best-practice enforcement. # Only channel-agnostic tests should go here since it is only run once # (on Flutter master). @@ -101,7 +84,9 @@ task: always: format_script: ./script/tool_runner.sh format --fail-on-change license_script: $PLUGIN_TOOL_COMMAND license-check - pubspec_script: ./script/tool_runner.sh pubspec-check + # The major and minor versions here should match the lowest version + # analyzed in legacy_version_analyze. + pubspec_script: ./script/tool_runner.sh pubspec-check --min-min-flutter-version=3.0.0 --min-min-dart-version=2.17.0 readme_script: - ./script/tool_runner.sh readme-check # Re-run with --require-excerpts, skipping packages that still need @@ -130,21 +115,11 @@ task: - else - echo "Only run in presubmit" - fi - - name: dart_unit_tests - env: - matrix: - CHANNEL: "master" - CHANNEL: "stable" - unit_test_script: - - ./script/tool_runner.sh test - name: analyze env: matrix: CHANNEL: "master" CHANNEL: "stable" - analyze_tool_script: - - cd script/tool - - dart analyze --fatal-infos analyze_script: # DO NOT change the custom-analysis argument here without changing the Dart repo. # See the comment in script/configs/custom_analysis.yaml for details. @@ -171,12 +146,13 @@ task: - name: legacy_version_analyze depends_on: analyze matrix: + # Change the arguments to pubspec-check when changing these values. env: CHANNEL: "3.0.5" DART_VERSION: "2.17.6" env: - CHANNEL: "2.10.5" - DART_VERSION: "2.16.2" + CHANNEL: "3.3.10" + DART_VERSION: "2.18.6" package_prep_script: # Allow analyzing packages that use a dev dependency with a higher # minimum Flutter/Dart version than the package itself. @@ -198,21 +174,21 @@ task: CIRRUS_CLONE_SUBMODULES: true script: ./script/tool_runner.sh update-excerpts --fail-on-change ### Web tasks ### - - name: web-build_all_plugins + - name: web-build_all_packages env: BUILD_ALL_ARGS: "web" matrix: CHANNEL: "master" CHANNEL: "stable" - << : *BUILD_ALL_PLUGINS_APP_TEMPLATE + << : *BUILD_ALL_PACKAGES_APP_TEMPLATE ### Linux desktop tasks ### - - name: linux-build_all_plugins + - name: linux-build_all_packages env: BUILD_ALL_ARGS: "linux" matrix: CHANNEL: "master" CHANNEL: "stable" - << : *BUILD_ALL_PLUGINS_APP_TEMPLATE + << : *BUILD_ALL_PACKAGES_APP_TEMPLATE - name: linux-platform_tests # Don't run full platform tests on both channels in pre-submit. skip: $CIRRUS_PR != '' && $CHANNEL == 'stable' @@ -240,8 +216,16 @@ task: zone: us-central1-a namespace: default cpu: 4 - memory: 12G + memory: 16G matrix: + ### Platform-agnostic tasks ### + - name: dart_unit_tests + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + unit_test_script: + - ./script/tool_runner.sh test ### Android tasks ### - name: android-platform_tests # Don't run full platform tests on both channels in pre-submit. @@ -257,13 +241,13 @@ task: CHANNEL: "master" CHANNEL: "stable" MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] - GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[4457646586de940f49e054de7d82e60078b205ac627f11a89d077e63f639c9ba1002541d9209a9ee7777e159e97b43d0] + GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[df5cf97036c09184e386edbf4ab1e741189e0ac5ca7e4c73673c4bf02d8709c9ac733597e8f5b6511b51eafb52e4027f] build_script: - ./script/tool_runner.sh build-examples --apk lint_script: - ./script/tool_runner.sh lint-android # must come after build-examples native_unit_test_script: - # Native integration tests are handled by firebase-test-lab below, so + # Native integration tests are handled by Firebase Test Lab below, so # only run unit tests. # Must come after build-examples. - ./script/tool_runner.sh native-test --android --no-integration --exclude script/configs/exclude_native_unit_android.yaml @@ -280,13 +264,13 @@ task: path: "**/reports/lint-results-debug.xml" type: text/xml format: android-lint - - name: android-build_all_plugins + - name: android-build_all_packages env: BUILD_ALL_ARGS: "apk" matrix: CHANNEL: "master" CHANNEL: "stable" - << : *BUILD_ALL_PLUGINS_APP_TEMPLATE + << : *BUILD_ALL_PACKAGES_APP_TEMPLATE ### Web tasks ### - name: web-platform_tests env: @@ -303,37 +287,3 @@ task: - ./script/tool_runner.sh build-examples --web drive_script: - ./script/tool_runner.sh drive-examples --web --exclude=script/configs/exclude_integration_web.yaml - -# ARM macOS tasks. -task: - << : *MACOS_ARM_TEMPLATE - << : *FLUTTER_UPGRADE_TEMPLATE - matrix: - ### iOS tasks ### - - name: ios-build_all_plugins - env: - BUILD_ALL_ARGS: "ios --no-codesign" - matrix: - CHANNEL: "master" - CHANNEL: "stable" - << : *BUILD_ALL_PLUGINS_APP_TEMPLATE - ### macOS desktop tasks ### - - name: macos-platform_tests - # Don't run full platform tests on both channels in pre-submit. - skip: $CIRRUS_PR != '' && $CHANNEL == 'stable' - env: - matrix: - CHANNEL: "master" - CHANNEL: "stable" - PATH: $PATH:/usr/local/bin - build_script: - - ./script/tool_runner.sh build-examples --macos - xcode_analyze_script: - - ./script/tool_runner.sh xcode-analyze --macos - xcode_analyze_deprecation_script: - # Ensure we don't accidentally introduce deprecated code. - - ./script/tool_runner.sh xcode-analyze --macos --macos-min-version=12.3 - native_test_script: - - ./script/tool_runner.sh native-test --macos - drive_script: - - ./script/tool_runner.sh drive-examples --macos --exclude=script/configs/exclude_integration_macos.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bbb153386ff2..532987f931df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,8 +31,7 @@ jobs: with: fetch-depth: 0 # Fetch all history so the tool can get all the tags to determine version. - name: Set up tools - run: dart pub get - working-directory: ${{ github.workspace }}/script/tool + run: dart pub global activate flutter_plugin_tools 0.13.4+3 # This workflow should be the last to run. So wait for all the other tests to succeed. - name: Wait on all tests @@ -50,5 +49,5 @@ jobs: run: | git config --global user.name ${{ secrets.USER_NAME }} git config --global user.email ${{ secrets.USER_EMAIL }} - dart ./script/tool/lib/src/main.dart publish --all-changed --base-sha=HEAD~ --skip-confirmation --remote=origin + dart pub global run flutter_plugin_tools publish --all-changed --base-sha=HEAD~ --skip-confirmation --remote=origin env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"} diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index f50f8d09dcd5..f0f36ab9d96c 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -42,7 +42,7 @@ jobs: # Upload the results as artifacts (optional). - name: "Upload artifact" - uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce with: name: SARIF file path: results.sarif @@ -50,6 +50,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@959cbb7472c4d4ad70cdfe6f4976053fe48ab394 + uses: github/codeql-action/upload-sarif@3ebbd71c74ef574dbc558c82f70e52732c8b44fe with: sarif_file: results.sarif diff --git a/CODEOWNERS b/CODEOWNERS index f128098711f9..603e4a24fcc0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -9,15 +9,15 @@ packages/camera/** @bparrishMines packages/file_selector/** @stuartmorgan packages/google_maps_flutter/** @stuartmorgan packages/google_sign_in/** @stuartmorgan -packages/image_picker/** @stuartmorgan +packages/image_picker/** @tarrinneal packages/in_app_purchase/** @bparrishMines packages/local_auth/** @stuartmorgan -packages/path_provider/** @gaaclarke +packages/path_provider/** @stuartmorgan packages/plugin_platform_interface/** @stuartmorgan -packages/quick_actions/** @stuartmorgan +packages/quick_actions/** @bparrishMines packages/shared_preferences/** @tarrinneal packages/url_launcher/** @stuartmorgan -packages/video_player/** @gaaclarke +packages/video_player/** @tarrinneal packages/webview_flutter/** @bparrishMines # Sub-package-level rules. These should stay last, since the last matching @@ -28,27 +28,29 @@ packages/**/*_web/** @ditman # - Android packages/camera/camera_android/** @camsim99 -packages/espresso/** @GaryQian -packages/flutter_plugin_android_lifecycle/** @GaryQian -packages/google_maps_flutter/google_maps_flutter_android/** @GaryQian +packages/camera/camera_android_camerax/** @camsim99 +packages/espresso/** @reidbaker +packages/flutter_plugin_android_lifecycle/** @reidbaker +packages/google_maps_flutter/google_maps_flutter_android/** @reidbaker packages/google_sign_in/google_sign_in_android/** @camsim99 -packages/image_picker/image_picker_android/** @GaryQian -packages/in_app_purchase/in_app_purchase_android/** @GaryQian +packages/image_picker/image_picker_android/** @gmackall +packages/in_app_purchase/in_app_purchase_android/** @gmackall packages/local_auth/local_auth_android/** @camsim99 packages/path_provider/path_provider_android/** @camsim99 packages/quick_actions/quick_actions_android/** @camsim99 -packages/url_launcher/url_launcher_android/** @GaryQian +packages/shared_preferences/shared_preferences_android/** @reidbaker +packages/url_launcher/url_launcher_android/** @gmackall packages/video_player/video_player_android/** @camsim99 # - iOS packages/camera/camera_avfoundation/** @hellohuanlin packages/file_selector/file_selector_ios/** @jmagman packages/google_maps_flutter/google_maps_flutter_ios/** @cyanglaz -packages/google_sign_in/google_sign_in_ios/** @jmagman +packages/google_sign_in/google_sign_in_ios/** @vashworth packages/image_picker/image_picker_ios/** @vashworth packages/in_app_purchase/in_app_purchase_storekit/** @cyanglaz packages/ios_platform_images/ios/** @jmagman -packages/local_auth/local_auth_ios/** @hellohuanlin +packages/local_auth/local_auth_ios/** @louisehsu packages/path_provider/path_provider_foundation/** @jmagman packages/quick_actions/quick_actions_ios/** @hellohuanlin packages/shared_preferences/shared_preferences_foundation/** @cyanglaz diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2d44d50049b..8441f06a5884 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,10 @@ # Contributing to Flutter Plugins -[![Build Status](https://api.cirrus-ci.com/github/flutter/plugins.svg)](https://cirrus-ci.com/github/flutter/plugins/main) +| **ARCHIVED** | +|--------------| +| This repository is no longer in use; all source has moved to [flutter/packages](https://github.com/flutter/packages) and future development work will be done there. | + +## ARCHIVED CONTENT _See also: [Flutter's code of conduct](https://github.com/flutter/flutter/blob/master/CODE_OF_CONDUCT.md)_ diff --git a/README.md b/README.md index 92098af809e9..df38e848a6ae 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # Flutter plugins -[![Build Status](https://api.cirrus-ci.com/github/flutter/plugins.svg)](https://cirrus-ci.com/github/flutter/plugins/main) -[![Release Status](https://github.com/flutter/plugins/actions/workflows/release.yml/badge.svg)](https://github.com/flutter/plugins/actions/workflows/release.yml) -[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/flutter/plugins/badge)](https://api.securityscorecards.dev/projects/github.com/flutter/plugins) +| **ARCHIVED** | +|--------------| +| This repository is no longer in use; all source has moved to [flutter/packages](https://github.com/flutter/packages) and future development work will be done there. | + +## ARCHIVED CONTENT This repo is a companion repo to the main [flutter repo](https://github.com/flutter/flutter). It contains the source code for diff --git a/analysis_options.yaml b/analysis_options.yaml index 85f5bde1b0de..498d19dfb4ae 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,55 +1,24 @@ -# This is a copy (as of March 2021) of flutter/flutter's analysis_options file, -# with minimal changes for this repository. The goal is to move toward using a -# shared set of analysis options as much as possible, and eventually a shared -# file. - # Specify analysis options. # -# For a list of lints, see: http://dart-lang.github.io/linter/lints/ -# See the configuration guide for more -# https://github.com/dart-lang/sdk/tree/main/pkg/analyzer#configuring-the-analyzer -# -# There are other similar analysis options files in the flutter repos, -# which should be kept in sync with this file: -# -# - analysis_options.yaml (this file) -# - packages/flutter/lib/analysis_options_user.yaml -# - https://github.com/flutter/flutter/blob/master/analysis_options.yaml -# - https://github.com/flutter/engine/blob/main/analysis_options.yaml -# - https://github.com/flutter/packages/blob/main/analysis_options.yaml -# -# This file contains the analysis options used for code in the flutter/plugins -# repository. +# This file is a copy of analysis_options.yaml from flutter repo +# as of 2022-07-27, but with some modifications marked with +# "DIFFERENT FROM FLUTTER/FLUTTER" below. The file is expected to +# be kept in sync with the master file from the flutter repo. analyzer: language: strict-casts: true strict-raw-types: true errors: - # treat missing required parameters as a warning (not a hint) - missing_required_param: warning - # treat missing returns as a warning (not a hint) - missing_return: warning - # allow having TODO comments in the code - todo: ignore # allow self-reference to deprecated members (we do this because otherwise we have # to annotate every member in every test, assert, etc, when we deprecate something) deprecated_member_use_from_same_package: ignore - # Ignore analyzer hints for updating pubspecs when using Future or - # Stream and not importing dart:async - # Please see https://github.com/flutter/flutter/pull/24528 for details. - sdk_version_async_exported_from_core: ignore # Turned off until null-safe rollout is complete. unnecessary_null_comparison: ignore - ### Local flutter/plugins changes ### - # Allow null checks for as long as mixed mode is officially supported. - always_require_non_null_named_parameters: false # not needed with nnbd - exclude: + exclude: # DIFFERENT FROM FLUTTER/FLUTTER # Ignore generated files - '**/*.g.dart' - - 'lib/src/generated/*.dart' - '**/*.mocks.dart' # Mockito @GenerateMocks - - '**/*.pigeon.dart' # Pigeon generated file linter: rules: @@ -141,19 +110,19 @@ linter: # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/linter/issues/453 - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list - # - no_default_cases # LOCAL CHANGE - Needs to be enabled and violations fixed. + - no_default_cases - no_duplicate_case_values - no_leading_underscores_for_library_prefixes - no_leading_underscores_for_local_identifiers - no_logic_in_create_state - # - no_runtimeType_toString # ok in tests; we enable this only in packages/ + - no_runtimeType_toString # DIFFERENT FROM FLUTTER/FLUTTER - non_constant_identifier_names - noop_primitive_operations - null_check_on_nullable_type_parameter - null_closures # - omit_local_variable_types # opposite of always_specify_types # - one_member_abstracts # too many false positives - # - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al # LOCAL CHANGE - Needs to be enabled and violations fixed. + - only_throw_errors # this does get disabled in a few places where we have legacy code that uses strings et al - overridden_fields - package_api_docs - package_names @@ -200,7 +169,7 @@ linter: - prefer_typing_uninitialized_variables - prefer_void_to_null - provide_deprecation_message - # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml + - public_member_api_docs # DIFFERENT FROM FLUTTER/FLUTTER - recursive_getters # - require_trailing_commas # blocked on https://github.com/dart-lang/sdk/issues/47441 - secure_pubspec_urls @@ -241,7 +210,7 @@ linter: - unnecessary_to_list_in_spreads - unrelated_type_equality_checks - unsafe_html - # - use_build_context_synchronously # LOCAL CHANGE - Needs to be enabled and violations fixed. + - use_build_context_synchronously # - use_colored_box # not yet tested # - use_decorated_box # not yet tested # - use_enums # not yet tested @@ -261,8 +230,3 @@ linter: # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review - valid_regexps - void_checks - ### Local flutter/plugins additions ### - # These are from flutter/flutter/packages, so will need to be preserved - # separately when moving to a shared file. - - no_runtimeType_toString # use objectRuntimeType from package:foundation - - public_member_api_docs # see https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#documentation-dartdocs-javadocs-etc diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index d19a528a769e..13c00402449a 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.10.3 + +* Adds back use of Optional type. + +## 0.10.2+1 + +* Updates code for stricter lint checks. + ## 0.10.2 * Implements option to also stream when recording a video. @@ -31,7 +39,7 @@ ## 0.10.0 * **Breaking Change** Bumps default camera_web package version, which updates permission exception code from `cameraPermission` to `CameraAccessDenied`. -* **Breaking Change** Bumps default camera_android package version, which updates permission exception code from `cameraPermission` to +* **Breaking Change** Bumps default camera_android package version, which updates permission exception code from `cameraPermission` to `CameraAccessDenied` and `AudioAccessDenied`. * Ignores unnecessary import warnings in preparation for [upcoming Flutter changes](https://github.com/flutter/flutter/pull/106316). diff --git a/packages/camera/camera/example/android/gradle.properties b/packages/camera/camera/example/android/gradle.properties index b253d8e5f746..d0448f163e41 100644 --- a/packages/camera/camera/example/android/gradle.properties +++ b/packages/camera/camera/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=false android.enableR8=true diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index 4911625ae08e..b343b6da9d89 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -31,9 +31,11 @@ IconData getCameraLensIcon(CameraLensDirection direction) { return Icons.camera_front; case CameraLensDirection.external: return Icons.camera; - default: - throw ArgumentError('Unknown lens direction'); } + // This enum is from a different package, so a new value could be added at + // any time. The example should keep working if that happens. + // ignore: dead_code + return Icons.camera; } void _logError(String code, String? message) { diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index b201074f3810..7a396c1589f9 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:collection'; import 'dart:math'; import 'package:camera_platform_interface/camera_platform_interface.dart'; @@ -160,10 +161,10 @@ class CameraValue { bool? exposurePointSupported, bool? focusPointSupported, DeviceOrientation? deviceOrientation, - DeviceOrientation? lockedCaptureOrientation, - DeviceOrientation? recordingOrientation, + Optional? lockedCaptureOrientation, + Optional? recordingOrientation, bool? isPreviewPaused, - DeviceOrientation? previewPauseOrientation, + Optional? previewPauseOrientation, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -180,12 +181,16 @@ class CameraValue { exposurePointSupported ?? this.exposurePointSupported, focusPointSupported: focusPointSupported ?? this.focusPointSupported, deviceOrientation: deviceOrientation ?? this.deviceOrientation, - lockedCaptureOrientation: - lockedCaptureOrientation ?? this.lockedCaptureOrientation, - recordingOrientation: recordingOrientation ?? this.recordingOrientation, + lockedCaptureOrientation: lockedCaptureOrientation == null + ? this.lockedCaptureOrientation + : lockedCaptureOrientation.orNull, + recordingOrientation: recordingOrientation == null + ? this.recordingOrientation + : recordingOrientation.orNull, isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, - previewPauseOrientation: - previewPauseOrientation ?? this.previewPauseOrientation, + previewPauseOrientation: previewPauseOrientation == null + ? this.previewPauseOrientation + : previewPauseOrientation.orNull, ); } @@ -353,8 +358,8 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.pausePreview(_cameraId); value = value.copyWith( isPreviewPaused: true, - previewPauseOrientation: - value.lockedCaptureOrientation ?? value.deviceOrientation); + previewPauseOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -367,7 +372,9 @@ class CameraController extends ValueNotifier { } try { await CameraPlatform.instance.resumePreview(_cameraId); - value = value.copyWith(isPreviewPaused: false); + value = value.copyWith( + isPreviewPaused: false, + previewPauseOrientation: const Optional.absent()); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -498,9 +505,9 @@ class CameraController extends ValueNotifier { value = value.copyWith( isRecordingVideo: true, isRecordingPaused: false, - isStreamingImages: onAvailable != null, - recordingOrientation: - value.lockedCaptureOrientation ?? value.deviceOrientation); + recordingOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation), + isStreamingImages: onAvailable != null); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -525,7 +532,10 @@ class CameraController extends ValueNotifier { try { final XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); - value = value.copyWith(isRecordingVideo: false); + value = value.copyWith( + isRecordingVideo: false, + recordingOrientation: const Optional.absent(), + ); return file; } on PlatformException catch (e) { throw CameraException(e.code, e.message); @@ -743,7 +753,8 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.lockCaptureOrientation( _cameraId, orientation ?? value.deviceOrientation); value = value.copyWith( - lockedCaptureOrientation: orientation ?? value.deviceOrientation); + lockedCaptureOrientation: Optional.of( + orientation ?? value.deviceOrientation)); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -763,7 +774,8 @@ class CameraController extends ValueNotifier { Future unlockCaptureOrientation() async { try { await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); - value = value.copyWith(); + value = value.copyWith( + lockedCaptureOrientation: const Optional.absent()); } on PlatformException catch (e) { throw CameraException(e.code, e.message); } @@ -834,3 +846,112 @@ class CameraController extends ValueNotifier { } } } + +/// A value that might be absent. +/// +/// Used to represent [DeviceOrientation]s that are optional but also able +/// to be cleared. +@immutable +class Optional extends IterableBase { + /// Constructs an empty Optional. + const Optional.absent() : _value = null; + + /// Constructs an Optional of the given [value]. + /// + /// Throws [ArgumentError] if [value] is null. + Optional.of(T value) : _value = value { + // TODO(cbracken): Delete and make this ctor const once mixed-mode + // execution is no longer around. + ArgumentError.checkNotNull(value); + } + + /// Constructs an Optional of the given [value]. + /// + /// If [value] is null, returns [absent()]. + const Optional.fromNullable(T? value) : _value = value; + + final T? _value; + + /// True when this optional contains a value. + bool get isPresent => _value != null; + + /// True when this optional contains no value. + bool get isNotPresent => _value == null; + + /// Gets the Optional value. + /// + /// Throws [StateError] if [value] is null. + T get value { + if (_value == null) { + throw StateError('value called on absent Optional.'); + } + return _value!; + } + + /// Executes a function if the Optional value is present. + void ifPresent(void Function(T value) ifPresent) { + if (isPresent) { + ifPresent(_value as T); + } + } + + /// Execution a function if the Optional value is absent. + void ifAbsent(void Function() ifAbsent) { + if (!isPresent) { + ifAbsent(); + } + } + + /// Gets the Optional value with a default. + /// + /// The default is returned if the Optional is [absent()]. + /// + /// Throws [ArgumentError] if [defaultValue] is null. + T or(T defaultValue) { + return _value ?? defaultValue; + } + + /// Gets the Optional value, or `null` if there is none. + T? get orNull => _value; + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// The transformer must not return `null`. If it does, an [ArgumentError] is thrown. + Optional transform(S Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.of(transformer(_value as T)); + } + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// Returns [absent()] if the transformer returns `null`. + Optional transformNullable(S? Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.fromNullable(transformer(_value as T)); + } + + @override + Iterator get iterator => + isPresent ? [_value as T].iterator : Iterable.empty().iterator; + + /// Delegates to the underlying [value] hashCode. + @override + int get hashCode => _value.hashCode; + + /// Delegates to the underlying [value] operator==. + @override + bool operator ==(Object o) => o is Optional && o._value == _value; + + @override + String toString() { + return _value == null + ? 'Optional { absent }' + : 'Optional { value: $_value }'; + } +} diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index f8b23bf09db8..1b902ab61f0a 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -4,7 +4,7 @@ description: A Flutter plugin for controlling the camera. Supports previewing Dart. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.10.2 +version: 0.10.3 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart index 7c4378749ebc..6677fcf90393 100644 --- a/packages/camera/camera/test/camera_preview_test.dart +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -133,8 +133,11 @@ void main() { isInitialized: true, isRecordingVideo: true, deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: DeviceOrientation.landscapeRight, - recordingOrientation: DeviceOrientation.landscapeLeft, + lockedCaptureOrientation: + const Optional.fromNullable( + DeviceOrientation.landscapeRight), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), previewSize: const Size(480, 640), ); @@ -164,8 +167,11 @@ void main() { controller.value = controller.value.copyWith( isInitialized: true, deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: DeviceOrientation.landscapeRight, - recordingOrientation: DeviceOrientation.landscapeLeft, + lockedCaptureOrientation: + const Optional.fromNullable( + DeviceOrientation.landscapeRight), + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), previewSize: const Size(480, 640), ); @@ -195,7 +201,8 @@ void main() { controller.value = controller.value.copyWith( isInitialized: true, deviceOrientation: DeviceOrientation.portraitUp, - recordingOrientation: DeviceOrientation.landscapeLeft, + recordingOrientation: const Optional.fromNullable( + DeviceOrientation.landscapeLeft), previewSize: const Size(480, 640), ); diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index 44a48d160d37..ab8354f7ba05 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -1166,7 +1166,8 @@ void main() { cameraController.value = cameraController.value.copyWith( isPreviewPaused: false, deviceOrientation: DeviceOrientation.portraitUp, - lockedCaptureOrientation: DeviceOrientation.landscapeRight); + lockedCaptureOrientation: + Optional.of(DeviceOrientation.landscapeRight)); await cameraController.pausePreview(); diff --git a/packages/camera/camera_android/CHANGELOG.md b/packages/camera/camera_android/CHANGELOG.md index dda28f0c96b4..4609b402058a 100644 --- a/packages/camera/camera_android/CHANGELOG.md +++ b/packages/camera/camera_android/CHANGELOG.md @@ -1,7 +1,20 @@ +## 0.10.4 + +* Temporarily fixes issue with requested video profiles being null by falling back to deprecated behavior in that case. + +## 0.10.3 + +* Adds back use of Optional type. +* Updates minimum Flutter version to 3.0. + +## 0.10.2+3 + +* Updates code for stricter lint checks. + ## 0.10.2+2 * Fixes zoom computation for virtual cameras hiding physical cameras in Android 11+. -* Removes the unused CameraZoom class from the codebase. +* Removes the unused CameraZoom class from the codebase. ## 0.10.2+1 diff --git a/packages/camera/camera_android/android/build.gradle b/packages/camera/camera_android/android/build.gradle index 4fbb2270b556..9c403e02bbd4 100644 --- a/packages/camera/camera_android/android/build.gradle +++ b/packages/camera/camera_android/android/build.gradle @@ -35,10 +35,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' baseline file("lint-baseline.xml") } compileOptions { @@ -63,7 +60,7 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.5.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'androidx.test:core:1.4.0' testImplementation 'org.robolectric:robolectric:4.5' } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java index 7c592b9c7e99..b02d6864b5b7 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -258,8 +258,11 @@ private void prepareMediaRecorder(String outputFilePath) throws IOException { MediaRecorderBuilder mediaRecorderBuilder; - if (Build.VERSION.SDK_INT >= 31) { - mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfile(), outputFilePath); + // TODO(camsim99): Revert changes that allow legacy code to be used when recordingProfile is null + // once this has largely been fixed on the Android side. https://github.com/flutter/flutter/issues/119668 + EncoderProfiles recordingProfile = getRecordingProfile(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && recordingProfile != null) { + mediaRecorderBuilder = new MediaRecorderBuilder(recordingProfile, outputFilePath); } else { mediaRecorderBuilder = new MediaRecorderBuilder(getRecordingProfileLegacy(), outputFilePath); } diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java index afbd7c3758a6..0ec2fbef87de 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java @@ -114,19 +114,23 @@ static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) if (preset.ordinal() > ResolutionPreset.high.ordinal()) { preset = ResolutionPreset.high; } - if (Build.VERSION.SDK_INT >= 31) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { EncoderProfiles profile = getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); List videoProfiles = profile.getVideoProfiles(); EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0); - return new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); - } else { - @SuppressWarnings("deprecation") - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + if (defaultVideoProfile != null) { + return new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); + } } + + @SuppressWarnings("deprecation") + // TODO(camsim99): Suppression is currently safe because legacy code is used as a fallback for SDK >= S. + // This should be removed when reverting that fallback behavior: https://github.com/flutter/flutter/issues/119668. + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); } /** @@ -234,15 +238,24 @@ private void configureResolution(ResolutionPreset resolutionPreset, int cameraId if (!checkIsSupported()) { return; } + boolean captureSizeCalculated = false; - if (Build.VERSION.SDK_INT >= 31) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + recordingProfileLegacy = null; recordingProfile = getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); List videoProfiles = recordingProfile.getVideoProfiles(); EncoderProfiles.VideoProfile defaultVideoProfile = videoProfiles.get(0); - captureSize = new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); - } else { + + if (defaultVideoProfile != null) { + captureSizeCalculated = true; + captureSize = new Size(defaultVideoProfile.getWidth(), defaultVideoProfile.getHeight()); + } + } + + if (!captureSizeCalculated) { + recordingProfile = null; @SuppressWarnings("deprecation") CamcorderProfile camcorderProfile = getBestAvailableCamcorderProfileForResolutionPresetLegacy(cameraId, resolutionPreset); diff --git a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java index 0aebfee39e0a..1f9f6200bb99 100644 --- a/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java +++ b/packages/camera/camera_android/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -75,7 +75,7 @@ public MediaRecorder build() throws IOException, NullPointerException, IndexOutO if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); - if (Build.VERSION.SDK_INT >= 31) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && encoderProfiles != null) { EncoderProfiles.VideoProfile videoProfile = encoderProfiles.getVideoProfiles().get(0); EncoderProfiles.AudioProfile audioProfile = encoderProfiles.getAudioProfiles().get(0); diff --git a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java index 957b57a66435..dbc352d697a4 100644 --- a/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java +++ b/packages/camera/camera_android/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java @@ -5,20 +5,27 @@ package io.flutter.plugins.camera.features.resolution; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; import android.media.CamcorderProfile; import android.media.EncoderProfiles; +import android.util.Size; import io.flutter.plugins.camera.CameraProperties; +import java.util.ArrayList; import java.util.List; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockedStatic; +import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -329,4 +336,95 @@ public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLow() { mockedStaticProfile.verify(() -> CamcorderProfile.getAll("1", CamcorderProfile.QUALITY_QVGA)); } + + @Config(minSdk = 31) + @Test + public void computeBestPreviewSize_shouldUseLegacyBehaviorWhenEncoderProfilesNull() { + try (MockedStatic mockedResolutionFeature = + mockStatic(ResolutionFeature.class)) { + mockedResolutionFeature + .when( + () -> + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPreset( + anyInt(), any(ResolutionPreset.class))) + .thenAnswer( + (Answer) + invocation -> { + EncoderProfiles mockEncoderProfiles = mock(EncoderProfiles.class); + List videoProfiles = + new ArrayList() { + { + add(null); + } + }; + when(mockEncoderProfiles.getVideoProfiles()).thenReturn(videoProfiles); + return mockEncoderProfiles; + }); + mockedResolutionFeature + .when( + () -> + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPresetLegacy( + anyInt(), any(ResolutionPreset.class))) + .thenAnswer( + (Answer) + invocation -> { + CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); + mockCamcorderProfile.videoFrameWidth = 10; + mockCamcorderProfile.videoFrameHeight = 50; + return mockCamcorderProfile; + }); + mockedResolutionFeature + .when(() -> ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max)) + .thenCallRealMethod(); + + Size testPreviewSize = ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + assertEquals(testPreviewSize.getWidth(), 10); + assertEquals(testPreviewSize.getHeight(), 50); + } + } + + @Config(minSdk = 31) + @Test + public void resolutionFeatureShouldUseLegacyBehaviorWhenEncoderProfilesNull() { + beforeLegacy(); + try (MockedStatic mockedResolutionFeature = + mockStatic(ResolutionFeature.class)) { + mockedResolutionFeature + .when( + () -> + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPreset( + anyInt(), any(ResolutionPreset.class))) + .thenAnswer( + (Answer) + invocation -> { + EncoderProfiles mockEncoderProfiles = mock(EncoderProfiles.class); + List videoProfiles = + new ArrayList() { + { + add(null); + } + }; + when(mockEncoderProfiles.getVideoProfiles()).thenReturn(videoProfiles); + return mockEncoderProfiles; + }); + mockedResolutionFeature + .when( + () -> + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPresetLegacy( + anyInt(), any(ResolutionPreset.class))) + .thenAnswer( + (Answer) + invocation -> { + CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); + return mockCamcorderProfile; + }); + + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + assertNotNull(resolutionFeature.getRecordingProfileLegacy()); + assertNull(resolutionFeature.getRecordingProfile()); + } + } } diff --git a/packages/camera/camera_android/example/android/gradle.properties b/packages/camera/camera_android/example/android/gradle.properties index b253d8e5f746..d0448f163e41 100644 --- a/packages/camera/camera_android/example/android/gradle.properties +++ b/packages/camera/camera_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=false android.enableR8=true diff --git a/packages/camera/camera_android/example/lib/camera_controller.dart b/packages/camera/camera_android/example/lib/camera_controller.dart index 79bf4e8b01e1..8139dcdb0220 100644 --- a/packages/camera/camera_android/example/lib/camera_controller.dart +++ b/packages/camera/camera_android/example/lib/camera_controller.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:collection'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; @@ -108,10 +109,10 @@ class CameraValue { bool? exposurePointSupported, bool? focusPointSupported, DeviceOrientation? deviceOrientation, - DeviceOrientation? lockedCaptureOrientation, - DeviceOrientation? recordingOrientation, + Optional? lockedCaptureOrientation, + Optional? recordingOrientation, bool? isPreviewPaused, - DeviceOrientation? previewPauseOrientation, + Optional? previewPauseOrientation, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -124,12 +125,16 @@ class CameraValue { exposureMode: exposureMode ?? this.exposureMode, focusMode: focusMode ?? this.focusMode, deviceOrientation: deviceOrientation ?? this.deviceOrientation, - lockedCaptureOrientation: - lockedCaptureOrientation ?? this.lockedCaptureOrientation, - recordingOrientation: recordingOrientation ?? this.recordingOrientation, + lockedCaptureOrientation: lockedCaptureOrientation == null + ? this.lockedCaptureOrientation + : lockedCaptureOrientation.orNull, + recordingOrientation: recordingOrientation == null + ? this.recordingOrientation + : recordingOrientation.orNull, isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, - previewPauseOrientation: - previewPauseOrientation ?? this.previewPauseOrientation, + previewPauseOrientation: previewPauseOrientation == null + ? this.previewPauseOrientation + : previewPauseOrientation.orNull, ); } @@ -257,14 +262,16 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.pausePreview(_cameraId); value = value.copyWith( isPreviewPaused: true, - previewPauseOrientation: - value.lockedCaptureOrientation ?? value.deviceOrientation); + previewPauseOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); } /// Resumes the current camera preview Future resumePreview() async { await CameraPlatform.instance.resumePreview(_cameraId); - value = value.copyWith(isPreviewPaused: false); + value = value.copyWith( + isPreviewPaused: false, + previewPauseOrientation: const Optional.absent()); } /// Captures an image and returns the file where it was saved. @@ -307,8 +314,8 @@ class CameraController extends ValueNotifier { isRecordingVideo: true, isRecordingPaused: false, isStreamingImages: streamCallback != null, - recordingOrientation: - value.lockedCaptureOrientation ?? value.deviceOrientation); + recordingOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); } /// Stops the video recording and returns the file where it was saved. @@ -324,6 +331,7 @@ class CameraController extends ValueNotifier { value = value.copyWith( isRecordingVideo: false, isRecordingPaused: false, + recordingOrientation: const Optional.absent(), ); return file; } @@ -392,12 +400,16 @@ class CameraController extends ValueNotifier { Future lockCaptureOrientation() async { await CameraPlatform.instance .lockCaptureOrientation(_cameraId, value.deviceOrientation); - value = value.copyWith(lockedCaptureOrientation: value.deviceOrientation); + value = value.copyWith( + lockedCaptureOrientation: + Optional.of(value.deviceOrientation)); } /// Unlocks the capture orientation. Future unlockCaptureOrientation() async { await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); + value = value.copyWith( + lockedCaptureOrientation: const Optional.absent()); } /// Sets the focus mode for taking pictures. @@ -431,3 +443,112 @@ class CameraController extends ValueNotifier { } } } + +/// A value that might be absent. +/// +/// Used to represent [DeviceOrientation]s that are optional but also able +/// to be cleared. +@immutable +class Optional extends IterableBase { + /// Constructs an empty Optional. + const Optional.absent() : _value = null; + + /// Constructs an Optional of the given [value]. + /// + /// Throws [ArgumentError] if [value] is null. + Optional.of(T value) : _value = value { + // TODO(cbracken): Delete and make this ctor const once mixed-mode + // execution is no longer around. + ArgumentError.checkNotNull(value); + } + + /// Constructs an Optional of the given [value]. + /// + /// If [value] is null, returns [absent()]. + const Optional.fromNullable(T? value) : _value = value; + + final T? _value; + + /// True when this optional contains a value. + bool get isPresent => _value != null; + + /// True when this optional contains no value. + bool get isNotPresent => _value == null; + + /// Gets the Optional value. + /// + /// Throws [StateError] if [value] is null. + T get value { + if (_value == null) { + throw StateError('value called on absent Optional.'); + } + return _value!; + } + + /// Executes a function if the Optional value is present. + void ifPresent(void Function(T value) ifPresent) { + if (isPresent) { + ifPresent(_value as T); + } + } + + /// Execution a function if the Optional value is absent. + void ifAbsent(void Function() ifAbsent) { + if (!isPresent) { + ifAbsent(); + } + } + + /// Gets the Optional value with a default. + /// + /// The default is returned if the Optional is [absent()]. + /// + /// Throws [ArgumentError] if [defaultValue] is null. + T or(T defaultValue) { + return _value ?? defaultValue; + } + + /// Gets the Optional value, or `null` if there is none. + T? get orNull => _value; + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// The transformer must not return `null`. If it does, an [ArgumentError] is thrown. + Optional transform(S Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.of(transformer(_value as T)); + } + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// Returns [absent()] if the transformer returns `null`. + Optional transformNullable(S? Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.fromNullable(transformer(_value as T)); + } + + @override + Iterator get iterator => + isPresent ? [_value as T].iterator : Iterable.empty().iterator; + + /// Delegates to the underlying [value] hashCode. + @override + int get hashCode => _value.hashCode; + + /// Delegates to the underlying [value] operator==. + @override + bool operator ==(Object o) => o is Optional && o._value == _value; + + @override + String toString() { + return _value == null + ? 'Optional { absent }' + : 'Optional { value: $_value }'; + } +} diff --git a/packages/camera/camera_android/example/lib/main.dart b/packages/camera/camera_android/example/lib/main.dart index af9aab1a8a86..4d98aed9a4c2 100644 --- a/packages/camera/camera_android/example/lib/main.dart +++ b/packages/camera/camera_android/example/lib/main.dart @@ -35,9 +35,11 @@ IconData getCameraLensIcon(CameraLensDirection direction) { return Icons.camera_front; case CameraLensDirection.external: return Icons.camera; - default: - throw ArgumentError('Unknown lens direction'); } + // This enum is from a different package, so a new value could be added at + // any time. The example should keep working if that happens. + // ignore: dead_code + return Icons.camera; } void _logError(String code, String? message) { @@ -1089,5 +1091,4 @@ Future main() async { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/camera/camera_android/example/pubspec.yaml b/packages/camera/camera_android/example/pubspec.yaml index 8c985d94fd5a..e23e31a886de 100644 --- a/packages/camera/camera_android/example/pubspec.yaml +++ b/packages/camera/camera_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: camera_android: diff --git a/packages/camera/camera_android/lib/src/android_camera.dart b/packages/camera/camera_android/lib/src/android_camera.dart index 1a9c3d0e1bdd..9ab9b578616a 100644 --- a/packages/camera/camera_android/lib/src/android_camera.dart +++ b/packages/camera/camera_android/lib/src/android_camera.dart @@ -145,6 +145,7 @@ class AndroidCamera extends CameraPlatform { // ignore: body_might_complete_normally_catch_error (Object error, StackTrace stackTrace) { if (error is! PlatformException) { + // ignore: only_throw_errors throw error; } completer.completeError( @@ -520,9 +521,14 @@ class AndroidCamera extends CameraPlatform { return 'always'; case FlashMode.torch: return 'torch'; - default: - throw ArgumentError('Unknown FlashMode value'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return 'off'; } /// Returns the resolution preset as a String. @@ -540,9 +546,14 @@ class AndroidCamera extends CameraPlatform { return 'medium'; case ResolutionPreset.low: return 'low'; - default: - throw ArgumentError('Unknown ResolutionPreset value'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return 'max'; } /// Converts messages received from the native platform into device events. diff --git a/packages/camera/camera_android/lib/src/utils.dart b/packages/camera/camera_android/lib/src/utils.dart index 663ec6da7a97..8d58f7fe1297 100644 --- a/packages/camera/camera_android/lib/src/utils.dart +++ b/packages/camera/camera_android/lib/src/utils.dart @@ -29,9 +29,14 @@ String serializeDeviceOrientation(DeviceOrientation orientation) { return 'landscapeRight'; case DeviceOrientation.landscapeLeft: return 'landscapeLeft'; - default: - throw ArgumentError('Unknown DeviceOrientation value'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return 'portraitUp'; } /// Returns the device orientation for a given String. diff --git a/packages/camera/camera_android/pubspec.yaml b/packages/camera/camera_android/pubspec.yaml index 20a86ad36c19..fb3371912911 100644 --- a/packages/camera/camera_android/pubspec.yaml +++ b/packages/camera/camera_android/pubspec.yaml @@ -2,11 +2,11 @@ name: camera_android description: Android implementation of the camera plugin. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.10.2+2 +version: 0.10.4 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/camera/camera_android/test/android_camera_test.dart b/packages/camera/camera_android/test/android_camera_test.dart index bd55b0b722ba..d80bd9cac7a3 100644 --- a/packages/camera/camera_android/test/android_camera_test.dart +++ b/packages/camera/camera_android/test/android_camera_test.dart @@ -32,14 +32,15 @@ void main() { // registerWith is called very early in initialization the bindings won't // have been initialized. While registerWith could intialize them, that // could slow down startup, so instead the handler should be set up lazily. - final ByteData? response = await TestDefaultBinaryMessengerBinding - .instance!.defaultBinaryMessenger - .handlePlatformMessage( - AndroidCamera.deviceEventChannelName, - const StandardMethodCodec().encodeMethodCall(const MethodCall( - 'orientation_changed', - {'orientation': 'portraitDown'})), - (ByteData? data) {}); + final ByteData? response = + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + AndroidCamera.deviceEventChannelName, + const StandardMethodCodec().encodeMethodCall(const MethodCall( + 'orientation_changed', + {'orientation': 'portraitDown'})), + (ByteData? data) {}); expect(response, null); }); @@ -421,7 +422,8 @@ void main() { const DeviceOrientationChangedEvent event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); for (int i = 0; i < 3; i++) { - await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( AndroidCamera.deviceEventChannelName, const StandardMethodCodec().encodeMethodCall( @@ -1121,3 +1123,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/camera/camera_android/test/method_channel_mock.dart b/packages/camera/camera_android/test/method_channel_mock.dart index 413c10633cc1..f26d12a3688a 100644 --- a/packages/camera/camera_android/test/method_channel_mock.dart +++ b/packages/camera/camera_android/test/method_channel_mock.dart @@ -11,7 +11,9 @@ class MethodChannelMock { this.delay, required this.methods, }) : methodChannel = MethodChannel(channelName) { - methodChannel.setMockMethodCallHandler(_handler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, _handler); } final Duration? delay; @@ -37,3 +39,9 @@ class MethodChannelMock { }); } } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index f94a86044ad8..9e6c5a901fc9 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -5,3 +5,10 @@ * Adds CameraSelector class. * Adds ProcessCameraProvider class. * Bump CameraX version to 1.3.0-alpha02. +* Adds Camera and UseCase classes, along with methods for binding UseCases to a lifecycle with the ProcessCameraProvider. +* Bump CameraX version to 1.3.0-alpha03 and Kotlin version to 1.8.0. +* Changes instance manager to allow the separate creation of identical objects. +* Adds Preview and Surface classes, along with other methods needed to implement camera preview. +* Adds implementation of availableCameras(). +* Implements camera preview, createCamera, initializeCamera, onCameraError, onDeviceOrientationChanged, and onCameraInitialized. +* Adds integration test to plugin. diff --git a/packages/camera/camera_android_camerax/android/build.gradle b/packages/camera/camera_android_camerax/android/build.gradle index 5eb60bb8ce64..822c3f6e318e 100644 --- a/packages/camera/camera_android_camerax/android/build.gradle +++ b/packages/camera/camera_android_camerax/android/build.gradle @@ -56,13 +56,13 @@ android { dependencies { // CameraX core library using the camera2 implementation must use same version number. - def camerax_version = "1.3.0-alpha02" + def camerax_version = "1.3.0-alpha03" implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}" implementation 'com.google.guava:guava:31.1-android' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'androidx.test:core:1.4.0' testImplementation 'org.robolectric:robolectric:4.8' } diff --git a/packages/camera/camera_android_camerax/android/src/main/AndroidManifest.xml b/packages/camera/camera_android_camerax/android/src/main/AndroidManifest.xml index ea4275c757cf..52012aaa6915 100644 --- a/packages/camera/camera_android_camerax/android/src/main/AndroidManifest.xml +++ b/packages/camera/camera_android_camerax/android/src/main/AndroidManifest.xml @@ -1,3 +1,8 @@ + + + + diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java index b8fbaf539c32..b61e7ac72224 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java @@ -6,16 +6,19 @@ import android.content.Context; import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleOwner; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.view.TextureRegistry; /** Platform implementation of the camera_plugin implemented with the CameraX library. */ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, ActivityAware { private InstanceManager instanceManager; private FlutterPluginBinding pluginBinding; private ProcessCameraProviderHostApiImpl processCameraProviderHostApi; + public SystemServicesHostApiImpl systemServicesHostApi; /** * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment. @@ -24,7 +27,7 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity */ public CameraAndroidCameraxPlugin() {} - void setUp(BinaryMessenger binaryMessenger, Context context) { + void setUp(BinaryMessenger binaryMessenger, Context context, TextureRegistry textureRegistry) { // Set up instance manager. instanceManager = InstanceManager.open( @@ -36,23 +39,23 @@ void setUp(BinaryMessenger binaryMessenger, Context context) { // Set up Host APIs. GeneratedCameraXLibrary.CameraInfoHostApi.setup( binaryMessenger, new CameraInfoHostApiImpl(instanceManager)); - GeneratedCameraXLibrary.JavaObjectHostApi.setup( - binaryMessenger, new JavaObjectHostApiImpl(instanceManager)); GeneratedCameraXLibrary.CameraSelectorHostApi.setup( binaryMessenger, new CameraSelectorHostApiImpl(binaryMessenger, instanceManager)); + GeneratedCameraXLibrary.JavaObjectHostApi.setup( + binaryMessenger, new JavaObjectHostApiImpl(instanceManager)); processCameraProviderHostApi = new ProcessCameraProviderHostApiImpl(binaryMessenger, instanceManager, context); GeneratedCameraXLibrary.ProcessCameraProviderHostApi.setup( binaryMessenger, processCameraProviderHostApi); + systemServicesHostApi = new SystemServicesHostApiImpl(binaryMessenger, instanceManager); + GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApi); + GeneratedCameraXLibrary.PreviewHostApi.setup( + binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry)); } @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) { pluginBinding = flutterPluginBinding; - (new CameraAndroidCameraxPlugin()) - .setUp( - flutterPluginBinding.getBinaryMessenger(), - flutterPluginBinding.getApplicationContext()); } @Override @@ -66,7 +69,16 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { @Override public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { - updateContext(activityPluginBinding.getActivity()); + setUp( + pluginBinding.getBinaryMessenger(), + pluginBinding.getApplicationContext(), + pluginBinding.getTextureRegistry()); + updateContext(pluginBinding.getApplicationContext()); + processCameraProviderHostApi.setLifecycleOwner( + (LifecycleOwner) activityPluginBinding.getActivity()); + systemServicesHostApi.setActivity(activityPluginBinding.getActivity()); + systemServicesHostApi.setPermissionsRegistry( + activityPluginBinding::addRequestPermissionsResultListener); } @Override @@ -89,7 +101,7 @@ public void onDetachedFromActivity() { * Updates context that is used to fetch the corresponding instance of a {@code * ProcessCameraProvider}. */ - private void updateContext(Context context) { + public void updateContext(Context context) { if (processCameraProviderHostApi != null) { processCameraProviderHostApi.setContext(context); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraFlutterApiImpl.java new file mode 100644 index 000000000000..a03548399485 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraFlutterApiImpl.java @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.camera.core.Camera; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraFlutterApi; + +public class CameraFlutterApiImpl extends CameraFlutterApi { + private final InstanceManager instanceManager; + + public CameraFlutterApiImpl(BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + void create(Camera camera, Reply reply) { + create(instanceManager.addHostCreatedInstance(camera), reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java index 7daba0d38d6a..d960b7fff70a 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoHostApiImpl.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.camera.core.CameraInfo; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraInfoHostApi; +import java.util.Objects; public class CameraInfoHostApiImpl implements CameraInfoHostApi { private final InstanceManager instanceManager; @@ -17,7 +18,8 @@ public CameraInfoHostApiImpl(InstanceManager instanceManager) { @Override public Long getSensorRotationDegrees(@NonNull Long identifier) { - CameraInfo cameraInfo = (CameraInfo) instanceManager.getInstance(identifier); + CameraInfo cameraInfo = + (CameraInfo) Objects.requireNonNull(instanceManager.getInstance(identifier)); return Long.valueOf(cameraInfo.getSensorRotationDegrees()); } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraPermissionsManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraPermissionsManager.java new file mode 100644 index 000000000000..19b1ee569a9b --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraPermissionsManager.java @@ -0,0 +1,120 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import android.Manifest; +import android.Manifest.permission; +import android.app.Activity; +import android.content.pm.PackageManager; +import androidx.annotation.VisibleForTesting; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +final class CameraPermissionsManager { + interface PermissionsRegistry { + @SuppressWarnings("deprecation") + void addListener( + io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener handler); + } + + interface ResultCallback { + void onResult(String errorCode, String errorDescription); + } + + /** + * Camera access permission errors handled when camera is created. See {@code MethodChannelCamera} + * in {@code camera/camera_platform_interface} for details. + */ + private static final String CAMERA_PERMISSIONS_REQUEST_ONGOING = + "CameraPermissionsRequestOngoing"; + + private static final String CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE = + "Another request is ongoing and multiple requests cannot be handled at once."; + private static final String CAMERA_ACCESS_DENIED = "CameraAccessDenied"; + private static final String CAMERA_ACCESS_DENIED_MESSAGE = "Camera access permission was denied."; + private static final String AUDIO_ACCESS_DENIED = "AudioAccessDenied"; + private static final String AUDIO_ACCESS_DENIED_MESSAGE = "Audio access permission was denied."; + + private static final int CAMERA_REQUEST_ID = 9796; + @VisibleForTesting boolean ongoing = false; + + void requestPermissions( + Activity activity, + PermissionsRegistry permissionsRegistry, + boolean enableAudio, + ResultCallback callback) { + if (ongoing) { + callback.onResult( + CAMERA_PERMISSIONS_REQUEST_ONGOING, CAMERA_PERMISSIONS_REQUEST_ONGOING_MESSAGE); + return; + } + if (!hasCameraPermission(activity) || (enableAudio && !hasAudioPermission(activity))) { + permissionsRegistry.addListener( + new CameraRequestPermissionsListener( + (String errorCode, String errorDescription) -> { + ongoing = false; + callback.onResult(errorCode, errorDescription); + })); + ongoing = true; + ActivityCompat.requestPermissions( + activity, + enableAudio + ? new String[] {Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO} + : new String[] {Manifest.permission.CAMERA}, + CAMERA_REQUEST_ID); + } else { + // Permissions already exist. Call the callback with success. + callback.onResult(null, null); + } + } + + private boolean hasCameraPermission(Activity activity) { + return ContextCompat.checkSelfPermission(activity, permission.CAMERA) + == PackageManager.PERMISSION_GRANTED; + } + + private boolean hasAudioPermission(Activity activity) { + return ContextCompat.checkSelfPermission(activity, permission.RECORD_AUDIO) + == PackageManager.PERMISSION_GRANTED; + } + + @VisibleForTesting + @SuppressWarnings("deprecation") + static final class CameraRequestPermissionsListener + implements io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener { + + // There's no way to unregister permission listeners in the v1 embedding, so we'll be called + // duplicate times in cases where the user denies and then grants a permission. Keep track of if + // we've responded before and bail out of handling the callback manually if this is a repeat + // call. + boolean alreadyCalled = false; + + final ResultCallback callback; + + @VisibleForTesting + CameraRequestPermissionsListener(ResultCallback callback) { + this.callback = callback; + } + + @Override + public boolean onRequestPermissionsResult(int id, String[] permissions, int[] grantResults) { + if (alreadyCalled || id != CAMERA_REQUEST_ID) { + return false; + } + + alreadyCalled = true; + // grantResults could be empty if the permissions request with the user is interrupted + // https://developer.android.com/reference/android/app/Activity#onRequestPermissionsResult(int,%20java.lang.String[],%20int[]) + if (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) { + callback.onResult(CAMERA_ACCESS_DENIED, CAMERA_ACCESS_DENIED_MESSAGE); + } else if (grantResults.length > 1 && grantResults[1] != PackageManager.PERMISSION_GRANTED) { + callback.onResult(AUDIO_ACCESS_DENIED, AUDIO_ACCESS_DENIED_MESSAGE); + } else { + callback.onResult(null, null); + } + return true; + } + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java index 9c559a72e63c..603f7cf78def 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraSelectorHostApiImpl.java @@ -12,6 +12,7 @@ import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraSelectorHostApi; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class CameraSelectorHostApiImpl implements CameraSelectorHostApi { private final BinaryMessenger binaryMessenger; @@ -41,23 +42,22 @@ public void create(@NonNull Long identifier, Long lensFacing) { @Override public List filter(@NonNull Long identifier, @NonNull List cameraInfoIds) { - CameraSelector cameraSelector = (CameraSelector) instanceManager.getInstance(identifier); + CameraSelector cameraSelector = + (CameraSelector) Objects.requireNonNull(instanceManager.getInstance(identifier)); List cameraInfosForFilter = new ArrayList(); for (Number cameraInfoAsNumber : cameraInfoIds) { Long cameraInfoId = cameraInfoAsNumber.longValue(); - CameraInfo cameraInfo = (CameraInfo) instanceManager.getInstance(cameraInfoId); + CameraInfo cameraInfo = + (CameraInfo) Objects.requireNonNull(instanceManager.getInstance(cameraInfoId)); cameraInfosForFilter.add(cameraInfo); } List filteredCameraInfos = cameraSelector.filter(cameraInfosForFilter); - final CameraInfoFlutterApiImpl cameraInfoFlutterApiImpl = - new CameraInfoFlutterApiImpl(binaryMessenger, instanceManager); List filteredCameraInfosIds = new ArrayList(); for (CameraInfo cameraInfo : filteredCameraInfos) { - cameraInfoFlutterApiImpl.create(cameraInfo, result -> {}); Long filteredCameraInfoId = instanceManager.getIdentifierForStrongReference(cameraInfo); filteredCameraInfosIds.add(filteredCameraInfoId); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java index 8063866d2fc6..4a3d277a4dc3 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXProxy.java @@ -4,10 +4,48 @@ package io.flutter.plugins.camerax; +import android.app.Activity; +import android.graphics.SurfaceTexture; +import android.view.Surface; +import androidx.annotation.NonNull; import androidx.camera.core.CameraSelector; +import androidx.camera.core.Preview; +import io.flutter.plugin.common.BinaryMessenger; +/** Utility class used to create CameraX-related objects primarily for testing purposes. */ public class CameraXProxy { public CameraSelector.Builder createCameraSelectorBuilder() { return new CameraSelector.Builder(); } + + public CameraPermissionsManager createCameraPermissionsManager() { + return new CameraPermissionsManager(); + } + + public DeviceOrientationManager createDeviceOrientationManager( + @NonNull Activity activity, + @NonNull Boolean isFrontFacing, + @NonNull int sensorOrientation, + @NonNull DeviceOrientationManager.DeviceOrientationChangeCallback callback) { + return new DeviceOrientationManager(activity, isFrontFacing, sensorOrientation, callback); + } + + public Preview.Builder createPreviewBuilder() { + return new Preview.Builder(); + } + + public Surface createSurface(@NonNull SurfaceTexture surfaceTexture) { + return new Surface(surfaceTexture); + } + + /** + * Creates an instance of the {@code SystemServicesFlutterApiImpl}. + * + *

Included in this class to utilize the callback methods it provides, e.g. {@code + * onCameraError(String)}. + */ + public SystemServicesFlutterApiImpl createSystemServicesFlutterApiImpl( + @NonNull BinaryMessenger binaryMessenger) { + return new SystemServicesFlutterApiImpl(binaryMessenger); + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java new file mode 100644 index 000000000000..ebcb86433f65 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java @@ -0,0 +1,329 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +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.view.Display; +import android.view.Surface; +import android.view.WindowManager; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; + +/** + * Support class to help to determine the media orientation based on the orientation of the device. + */ +public class DeviceOrientationManager { + + interface DeviceOrientationChangeCallback { + void onChange(DeviceOrientation newOrientation); + } + + private static final IntentFilter orientationIntentFilter = + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + + private final Activity activity; + private final boolean isFrontFacing; + private final int sensorOrientation; + private final DeviceOrientationChangeCallback deviceOrientationChangeCallback; + private PlatformChannel.DeviceOrientation lastOrientation; + private BroadcastReceiver broadcastReceiver; + + DeviceOrientationManager( + @NonNull Activity activity, + boolean isFrontFacing, + int sensorOrientation, + DeviceOrientationChangeCallback callback) { + this.activity = activity; + this.isFrontFacing = isFrontFacing; + this.sensorOrientation = sensorOrientation; + this.deviceOrientationChangeCallback = callback; + } + + /** + * Starts listening to the device's sensors or UI for orientation updates. + * + *

When orientation information is updated, the callback method of the {@link + * DeviceOrientationChangeCallback} is called with the new orientation. This latest value can also + * be retrieved through the {@link #getVideoOrientation()} accessor. + * + *

If the device's ACCELEROMETER_ROTATION setting is enabled the {@link + * DeviceOrientationManager} will report orientation updates based on the sensor information. If + * the ACCELEROMETER_ROTATION is disabled the {@link DeviceOrientationManager} will fallback to + * the deliver orientation updates based on the UI orientation. + */ + public void start() { + if (broadcastReceiver != null) { + return; + } + broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleUIOrientationChange(); + } + }; + activity.registerReceiver(broadcastReceiver, orientationIntentFilter); + broadcastReceiver.onReceive(activity, null); + } + + /** Stops listening for orientation updates. */ + public void stop() { + if (broadcastReceiver == null) { + return; + } + activity.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + /** + * Returns the device's photo orientation in degrees based on the sensor orientation and the last + * known UI orientation. + * + *

Returns one of 0, 90, 180 or 270. + * + * @return The device's photo orientation in degrees. + */ + public int getPhotoOrientation() { + return this.getPhotoOrientation(this.lastOrientation); + } + + /** + * Returns the device's photo orientation in degrees based on the sensor orientation and the + * supplied {@link PlatformChannel.DeviceOrientation} value. + * + *

Returns one of 0, 90, 180 or 270. + * + * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted + * into degrees. + * @return The device's photo orientation in degrees. + */ + public int getPhotoOrientation(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 = 90; + break; + case PORTRAIT_DOWN: + angle = 270; + break; + case LANDSCAPE_LEFT: + angle = isFrontFacing ? 180 : 0; + break; + case LANDSCAPE_RIGHT: + angle = isFrontFacing ? 0 : 180; + break; + } + + // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X). + // This has to be taken into account so the JPEG is rotated properly. + // For devices with orientation of 90, this simply returns the mapping from ORIENTATIONS. + // For devices with orientation of 270, the JPEG is rotated 180 degrees instead. + return (angle + sensorOrientation + 270) % 360; + } + + /** + * Returns the device's video orientation in clockwise degrees based on the sensor orientation and + * the last known UI orientation. + * + *

Returns one of 0, 90, 180 or 270. + * + * @return The device's video orientation in clockwise degrees. + */ + public int getVideoOrientation() { + return this.getVideoOrientation(this.lastOrientation); + } + + /** + * Returns the device's video orientation in clockwise degrees based on the sensor orientation and + * the supplied {@link PlatformChannel.DeviceOrientation} value. + * + *

Returns one of 0, 90, 180 or 270. + * + *

More details can be found in the official Android documentation: + * https://developer.android.com/reference/android/media/MediaRecorder#setOrientationHint(int) + * + *

See also: + * https://developer.android.com/training/camera2/camera-preview-large-screens#orientation_calculation + * + * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted + * into degrees. + * @return The device's video orientation in clockwise degrees. + */ + public int getVideoOrientation(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 = 270; + break; + case LANDSCAPE_RIGHT: + angle = 90; + break; + } + + if (isFrontFacing) { + angle *= -1; + } + + return (angle + sensorOrientation + 360) % 360; + } + + /** @return the last received UI orientation. */ + public PlatformChannel.DeviceOrientation getLastUIOrientation() { + return this.lastOrientation; + } + + /** + * Handles orientation changes based on change events triggered by the OrientationIntentFilter. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + */ + @VisibleForTesting + void handleUIOrientationChange() { + PlatformChannel.DeviceOrientation orientation = getUIOrientation(); + handleOrientationChange(orientation, lastOrientation, deviceOrientationChangeCallback); + lastOrientation = orientation; + } + + /** + * Handles orientation changes coming from either the device's sensors or the + * OrientationIntentFilter. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + */ + @VisibleForTesting + static void handleOrientationChange( + DeviceOrientation newOrientation, + DeviceOrientation previousOrientation, + DeviceOrientationChangeCallback callback) { + if (!newOrientation.equals(previousOrientation)) { + callback.onChange(newOrientation); + } + } + + /** + * Gets the current user interface orientation. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @return The current user interface orientation. + */ + @VisibleForTesting + 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; + } + } + + /** + * Calculates the sensor orientation based on the supplied angle. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @param angle Orientation angle. + * @return The sensor orientation based on the supplied angle. + */ + @VisibleForTesting + PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { + final int tolerance = 45; + angle += tolerance; + + // Orientation is 0 in the default orientation mode. This is portrait-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]; + } + + /** + * Gets the default orientation of the device. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @return The default orientation of the device. + */ + @VisibleForTesting + 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; + } + } + + /** + * Gets an instance of the Android {@link android.view.Display}. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @return An instance of the Android {@link android.view.Display}. + */ + @SuppressWarnings("deprecation") + @VisibleForTesting + Display getDisplay() { + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java index 041564c3bfcb..1e61ea699292 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java @@ -13,6 +13,8 @@ import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MessageCodec; import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -23,6 +25,154 @@ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) public class GeneratedCameraXLibrary { + /** Generated class from Pigeon that represents data sent in messages. */ + public static class ResolutionInfo { + private @NonNull Long width; + + public @NonNull Long getWidth() { + return width; + } + + public void setWidth(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"width\" is null."); + } + this.width = setterArg; + } + + private @NonNull Long height; + + public @NonNull Long getHeight() { + return height; + } + + public void setHeight(@NonNull Long setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"height\" is null."); + } + this.height = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private ResolutionInfo() {} + + public static final class Builder { + private @Nullable Long width; + + public @NonNull Builder setWidth(@NonNull Long setterArg) { + this.width = setterArg; + return this; + } + + private @Nullable Long height; + + public @NonNull Builder setHeight(@NonNull Long setterArg) { + this.height = setterArg; + return this; + } + + public @NonNull ResolutionInfo build() { + ResolutionInfo pigeonReturn = new ResolutionInfo(); + pigeonReturn.setWidth(width); + pigeonReturn.setHeight(height); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("width", width); + toMapResult.put("height", height); + return toMapResult; + } + + static @NonNull ResolutionInfo fromMap(@NonNull Map map) { + ResolutionInfo pigeonResult = new ResolutionInfo(); + Object width = map.get("width"); + pigeonResult.setWidth( + (width == null) ? null : ((width instanceof Integer) ? (Integer) width : (Long) width)); + Object height = map.get("height"); + pigeonResult.setHeight( + (height == null) + ? null + : ((height instanceof Integer) ? (Integer) height : (Long) height)); + return pigeonResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class CameraPermissionsErrorData { + private @NonNull String errorCode; + + public @NonNull String getErrorCode() { + return errorCode; + } + + public void setErrorCode(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"errorCode\" is null."); + } + this.errorCode = setterArg; + } + + private @NonNull String description; + + public @NonNull String getDescription() { + return description; + } + + public void setDescription(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"description\" is null."); + } + this.description = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private CameraPermissionsErrorData() {} + + public static final class Builder { + private @Nullable String errorCode; + + public @NonNull Builder setErrorCode(@NonNull String setterArg) { + this.errorCode = setterArg; + return this; + } + + private @Nullable String description; + + public @NonNull Builder setDescription(@NonNull String setterArg) { + this.description = setterArg; + return this; + } + + public @NonNull CameraPermissionsErrorData build() { + CameraPermissionsErrorData pigeonReturn = new CameraPermissionsErrorData(); + pigeonReturn.setErrorCode(errorCode); + pigeonReturn.setDescription(description); + return pigeonReturn; + } + } + + @NonNull + Map toMap() { + Map toMapResult = new HashMap<>(); + toMapResult.put("errorCode", errorCode); + toMapResult.put("description", description); + return toMapResult; + } + + static @NonNull CameraPermissionsErrorData fromMap(@NonNull Map map) { + CameraPermissionsErrorData pigeonResult = new CameraPermissionsErrorData(); + Object errorCode = map.get("errorCode"); + pigeonResult.setErrorCode((String) errorCode); + Object description = map.get("description"); + pigeonResult.setDescription((String) description); + return pigeonResult; + } + } + public interface Result { void success(T result); @@ -332,6 +482,16 @@ public interface ProcessCameraProviderHostApi { @NonNull List getAvailableCameraInfos(@NonNull Long identifier); + @NonNull + Long bindToLifecycle( + @NonNull Long identifier, + @NonNull Long cameraSelectorIdentifier, + @NonNull List useCaseIds); + + void unbind(@NonNull Long identifier, @NonNull List useCaseIds); + + void unbindAll(@NonNull Long identifier); + /** The codec used by ProcessCameraProviderHostApi. */ static MessageCodec getCodec() { return ProcessCameraProviderHostApiCodec.INSTANCE; @@ -405,6 +565,107 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + Number cameraSelectorIdentifierArg = (Number) args.get(1); + if (cameraSelectorIdentifierArg == null) { + throw new NullPointerException( + "cameraSelectorIdentifierArg unexpectedly null."); + } + List useCaseIdsArg = (List) args.get(2); + if (useCaseIdsArg == null) { + throw new NullPointerException("useCaseIdsArg unexpectedly null."); + } + Long output = + api.bindToLifecycle( + (identifierArg == null) ? null : identifierArg.longValue(), + (cameraSelectorIdentifierArg == null) + ? null + : cameraSelectorIdentifierArg.longValue(), + useCaseIdsArg); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + List useCaseIdsArg = (List) args.get(1); + if (useCaseIdsArg == null) { + throw new NullPointerException("useCaseIdsArg unexpectedly null."); + } + api.unbind( + (identifierArg == null) ? null : identifierArg.longValue(), useCaseIdsArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ProcessCameraProviderHostApi.unbindAll", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + api.unbindAll((identifierArg == null) ? null : identifierArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } @@ -445,6 +706,400 @@ public void create(@NonNull Long identifierArg, Reply callback) { } } + private static class CameraFlutterApiCodec extends StandardMessageCodec { + public static final CameraFlutterApiCodec INSTANCE = new CameraFlutterApiCodec(); + + private CameraFlutterApiCodec() {} + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class CameraFlutterApi { + private final BinaryMessenger binaryMessenger; + + public CameraFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return CameraFlutterApiCodec.INSTANCE; + } + + public void create(@NonNull Long identifierArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.CameraFlutterApi.create", getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg)), + channelReply -> { + callback.reply(null); + }); + } + } + + private static class SystemServicesHostApiCodec extends StandardMessageCodec { + public static final SystemServicesHostApiCodec INSTANCE = new SystemServicesHostApiCodec(); + + private SystemServicesHostApiCodec() {} + + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return CameraPermissionsErrorData.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof CameraPermissionsErrorData) { + stream.write(128); + writeValue(stream, ((CameraPermissionsErrorData) value).toMap()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface SystemServicesHostApi { + void requestCameraPermissions( + @NonNull Boolean enableAudio, Result result); + + void startListeningForDeviceOrientationChange( + @NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation); + + void stopListeningForDeviceOrientationChange(); + + /** The codec used by SystemServicesHostApi. */ + static MessageCodec getCodec() { + return SystemServicesHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `SystemServicesHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, SystemServicesHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesHostApi.requestCameraPermissions", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Boolean enableAudioArg = (Boolean) args.get(0); + if (enableAudioArg == null) { + throw new NullPointerException("enableAudioArg unexpectedly null."); + } + Result resultCallback = + new Result() { + public void success(CameraPermissionsErrorData result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.requestCameraPermissions(enableAudioArg, resultCallback); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Boolean isFrontFacingArg = (Boolean) args.get(0); + if (isFrontFacingArg == null) { + throw new NullPointerException("isFrontFacingArg unexpectedly null."); + } + Number sensorOrientationArg = (Number) args.get(1); + if (sensorOrientationArg == null) { + throw new NullPointerException("sensorOrientationArg unexpectedly null."); + } + api.startListeningForDeviceOrientationChange( + isFrontFacingArg, + (sensorOrientationArg == null) ? null : sensorOrientationArg.longValue()); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesHostApi.stopListeningForDeviceOrientationChange", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.stopListeningForDeviceOrientationChange(); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class SystemServicesFlutterApiCodec extends StandardMessageCodec { + public static final SystemServicesFlutterApiCodec INSTANCE = + new SystemServicesFlutterApiCodec(); + + private SystemServicesFlutterApiCodec() {} + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class SystemServicesFlutterApi { + private final BinaryMessenger binaryMessenger; + + public SystemServicesFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return SystemServicesFlutterApiCodec.INSTANCE; + } + + public void onDeviceOrientationChanged(@NonNull String orientationArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesFlutterApi.onDeviceOrientationChanged", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(orientationArg)), + channelReply -> { + callback.reply(null); + }); + } + + public void onCameraError(@NonNull String errorDescriptionArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(errorDescriptionArg)), + channelReply -> { + callback.reply(null); + }); + } + } + + private static class PreviewHostApiCodec extends StandardMessageCodec { + public static final PreviewHostApiCodec INSTANCE = new PreviewHostApiCodec(); + + private PreviewHostApiCodec() {} + + @Override + protected Object readValueOfType(byte type, ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return ResolutionInfo.fromMap((Map) readValue(buffer)); + + case (byte) 129: + return ResolutionInfo.fromMap((Map) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(ByteArrayOutputStream stream, Object value) { + if (value instanceof ResolutionInfo) { + stream.write(128); + writeValue(stream, ((ResolutionInfo) value).toMap()); + } else if (value instanceof ResolutionInfo) { + stream.write(129); + writeValue(stream, ((ResolutionInfo) value).toMap()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface PreviewHostApi { + void create( + @NonNull Long identifier, + @Nullable Long rotation, + @Nullable ResolutionInfo targetResolution); + + @NonNull + Long setSurfaceProvider(@NonNull Long identifier); + + void releaseFlutterSurfaceTexture(); + + @NonNull + ResolutionInfo getResolutionInfo(@NonNull Long identifier); + + /** The codec used by PreviewHostApi. */ + static MessageCodec getCodec() { + return PreviewHostApiCodec.INSTANCE; + } + + /** Sets up an instance of `PreviewHostApi` to handle messages through the `binaryMessenger`. */ + static void setup(BinaryMessenger binaryMessenger, PreviewHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PreviewHostApi.create", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + Number rotationArg = (Number) args.get(1); + ResolutionInfo targetResolutionArg = (ResolutionInfo) args.get(2); + api.create( + (identifierArg == null) ? null : identifierArg.longValue(), + (rotationArg == null) ? null : rotationArg.longValue(), + targetResolutionArg); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + Long output = + api.setSurfaceProvider( + (identifierArg == null) ? null : identifierArg.longValue()); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.PreviewHostApi.releaseFlutterSurfaceTexture", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + api.releaseFlutterSurfaceTexture(); + wrapped.put("result", null); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PreviewHostApi.getResolutionInfo", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + ResolutionInfo output = + api.getResolutionInfo( + (identifierArg == null) ? null : identifierArg.longValue()); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + private static Map wrapError(Throwable exception) { Map errorMap = new HashMap<>(); errorMap.put("message", exception.toString()); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java index 9b549d7bd1ea..8212d1267a19 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/InstanceManager.java @@ -122,16 +122,15 @@ public void addDartCreatedInstance(Object instance, long identifier) { /** * Adds a new instance that was instantiated from the host platform. * - *

If an instance has already been added, the identifier of the instance will be returned. + *

If an instance has already been added, this will replace it. {@code #containsInstance} can + * be used to check if the object has already been added to avoid this. * * @param instance the instance to be stored. * @return the unique identifier stored with instance. */ public long addHostCreatedInstance(Object instance) { assertManagerIsNotClosed(); - if (containsInstance(instance)) { - return getIdentifierForStrongReference(instance); - } + final long identifier = nextIdentifier++; addInstance(instance, identifier); return identifier; diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java new file mode 100644 index 000000000000..838f0b3d656c --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java @@ -0,0 +1,149 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import android.graphics.SurfaceTexture; +import android.util.Size; +import android.view.Surface; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.camera.core.Preview; +import androidx.camera.core.SurfaceRequest; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.PreviewHostApi; +import io.flutter.view.TextureRegistry; +import java.util.Objects; +import java.util.concurrent.Executors; + +public class PreviewHostApiImpl implements PreviewHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + private final TextureRegistry textureRegistry; + + @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); + @VisibleForTesting public TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture; + + public PreviewHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, + @NonNull InstanceManager instanceManager, + @NonNull TextureRegistry textureRegistry) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.textureRegistry = textureRegistry; + } + + /** Creates a {@link Preview} with the target rotation and resolution if specified. */ + @Override + public void create( + @NonNull Long identifier, + @Nullable Long rotation, + @Nullable GeneratedCameraXLibrary.ResolutionInfo targetResolution) { + Preview.Builder previewBuilder = cameraXProxy.createPreviewBuilder(); + if (rotation != null) { + previewBuilder.setTargetRotation(rotation.intValue()); + } + if (targetResolution != null) { + previewBuilder.setTargetResolution( + new Size( + targetResolution.getWidth().intValue(), targetResolution.getHeight().intValue())); + } + Preview preview = previewBuilder.build(); + instanceManager.addDartCreatedInstance(preview, identifier); + } + + /** + * Sets the {@link Preview.SurfaceProvider} that will be used to provide a {@code Surface} backed + * by a Flutter {@link TextureRegistry.SurfaceTextureEntry} used to build the {@link Preview}. + */ + @Override + public Long setSurfaceProvider(@NonNull Long identifier) { + Preview preview = (Preview) Objects.requireNonNull(instanceManager.getInstance(identifier)); + flutterSurfaceTexture = textureRegistry.createSurfaceTexture(); + SurfaceTexture surfaceTexture = flutterSurfaceTexture.surfaceTexture(); + Preview.SurfaceProvider surfaceProvider = createSurfaceProvider(surfaceTexture); + preview.setSurfaceProvider(surfaceProvider); + + return flutterSurfaceTexture.id(); + } + + /** + * Creates a {@link Preview.SurfaceProvider} that specifies how to provide a {@link Surface} to a + * {@code Preview} that is backed by a Flutter {@link TextureRegistry.SurfaceTextureEntry}. + */ + @VisibleForTesting + public Preview.SurfaceProvider createSurfaceProvider(@NonNull SurfaceTexture surfaceTexture) { + return new Preview.SurfaceProvider() { + @Override + public void onSurfaceRequested(SurfaceRequest request) { + surfaceTexture.setDefaultBufferSize( + request.getResolution().getWidth(), request.getResolution().getHeight()); + Surface flutterSurface = cameraXProxy.createSurface(surfaceTexture); + request.provideSurface( + flutterSurface, + Executors.newSingleThreadExecutor(), + (result) -> { + // See https://developer.android.com/reference/androidx/camera/core/SurfaceRequest.Result for documentation. + // Always attempt a release. + flutterSurface.release(); + int resultCode = result.getResultCode(); + switch (resultCode) { + case SurfaceRequest.Result.RESULT_REQUEST_CANCELLED: + case SurfaceRequest.Result.RESULT_WILL_NOT_PROVIDE_SURFACE: + case SurfaceRequest.Result.RESULT_SURFACE_ALREADY_PROVIDED: + case SurfaceRequest.Result.RESULT_SURFACE_USED_SUCCESSFULLY: + // Only need to release, do nothing. + break; + case SurfaceRequest.Result.RESULT_INVALID_SURFACE: // Intentional fall through. + default: + // Release and send error. + SystemServicesFlutterApiImpl systemServicesFlutterApi = + cameraXProxy.createSystemServicesFlutterApiImpl(binaryMessenger); + systemServicesFlutterApi.sendCameraError( + getProvideSurfaceErrorDescription(resultCode), reply -> {}); + break; + } + }); + }; + }; + } + + /** + * Returns an error description for each {@link SurfaceRequest.Result} that represents an error + * with providing a surface. + */ + private String getProvideSurfaceErrorDescription(@Nullable int resultCode) { + switch (resultCode) { + case SurfaceRequest.Result.RESULT_INVALID_SURFACE: + return resultCode + ": Provided surface could not be used by the camera."; + default: + return resultCode + ": Attempt to provide a surface resulted with unrecognizable code."; + } + } + + /** + * Releases the Flutter {@link TextureRegistry.SurfaceTextureEntry} if used to provide a surface + * for a {@link Preview}. + */ + @Override + public void releaseFlutterSurfaceTexture() { + if (flutterSurfaceTexture != null) { + flutterSurfaceTexture.release(); + } + } + + /** Returns the resolution information for the specified {@link Preview}. */ + @Override + public GeneratedCameraXLibrary.ResolutionInfo getResolutionInfo(@NonNull Long identifier) { + Preview preview = (Preview) Objects.requireNonNull(instanceManager.getInstance(identifier)); + Size resolution = preview.getResolutionInfo().getResolution(); + + GeneratedCameraXLibrary.ResolutionInfo.Builder resolutionInfo = + new GeneratedCameraXLibrary.ResolutionInfo.Builder() + .setWidth(Long.valueOf(resolution.getWidth())) + .setHeight(Long.valueOf(resolution.getHeight())); + return resolutionInfo.build(); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java index 19c5eb5b3f70..e7036e7090c1 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java @@ -6,20 +6,26 @@ import android.content.Context; import androidx.annotation.NonNull; +import androidx.camera.core.Camera; import androidx.camera.core.CameraInfo; +import androidx.camera.core.CameraSelector; +import androidx.camera.core.UseCase; import androidx.camera.lifecycle.ProcessCameraProvider; import androidx.core.content.ContextCompat; +import androidx.lifecycle.LifecycleOwner; import com.google.common.util.concurrent.ListenableFuture; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ProcessCameraProviderHostApi; import java.util.ArrayList; import java.util.List; +import java.util.Objects; public class ProcessCameraProviderHostApiImpl implements ProcessCameraProviderHostApi { private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; private Context context; + private LifecycleOwner lifecycleOwner; public ProcessCameraProviderHostApiImpl( BinaryMessenger binaryMessenger, InstanceManager instanceManager, Context context) { @@ -28,6 +34,10 @@ public ProcessCameraProviderHostApiImpl( this.context = context; } + public void setLifecycleOwner(LifecycleOwner lifecycleOwner) { + this.lifecycleOwner = lifecycleOwner; + } + /** * Sets the context that the {@code ProcessCameraProvider} will use to attach the lifecycle of the * camera to. @@ -40,8 +50,8 @@ public void setContext(Context context) { } /** - * Returns the instance of the ProcessCameraProvider to manage the lifecycle of the camera for the - * current {@code Context}. + * Returns the instance of the {@code ProcessCameraProvider} to manage the lifecycle of the camera + * for the current {@code Context}. */ @Override public void getInstance(GeneratedCameraXLibrary.Result result) { @@ -54,9 +64,9 @@ public void getInstance(GeneratedCameraXLibrary.Result result) { // Camera provider is now guaranteed to be available. ProcessCameraProvider processCameraProvider = processCameraProviderFuture.get(); + final ProcessCameraProviderFlutterApiImpl flutterApi = + new ProcessCameraProviderFlutterApiImpl(binaryMessenger, instanceManager); if (!instanceManager.containsInstance(processCameraProvider)) { - final ProcessCameraProviderFlutterApiImpl flutterApi = - new ProcessCameraProviderFlutterApiImpl(binaryMessenger, instanceManager); flutterApi.create(processCameraProvider, reply -> {}); } result.success(instanceManager.getIdentifierForStrongReference(processCameraProvider)); @@ -67,11 +77,11 @@ public void getInstance(GeneratedCameraXLibrary.Result result) { ContextCompat.getMainExecutor(context)); } - /** Returns cameras available to the ProcessCameraProvider. */ + /** Returns cameras available to the {@code ProcessCameraProvider}. */ @Override public List getAvailableCameraInfos(@NonNull Long identifier) { ProcessCameraProvider processCameraProvider = - (ProcessCameraProvider) instanceManager.getInstance(identifier); + (ProcessCameraProvider) Objects.requireNonNull(instanceManager.getInstance(identifier)); List availableCameras = processCameraProvider.getAvailableCameraInfos(); List availableCamerasIds = new ArrayList(); @@ -79,9 +89,68 @@ public List getAvailableCameraInfos(@NonNull Long identifier) { new CameraInfoFlutterApiImpl(binaryMessenger, instanceManager); for (CameraInfo cameraInfo : availableCameras) { - cameraInfoFlutterApi.create(cameraInfo, result -> {}); + if (!instanceManager.containsInstance(cameraInfo)) { + cameraInfoFlutterApi.create(cameraInfo, result -> {}); + } availableCamerasIds.add(instanceManager.getIdentifierForStrongReference(cameraInfo)); } return availableCamerasIds; } + + /** + * Binds specified {@code UseCase}s to the lifecycle of the {@code LifecycleOwner} that + * corresponds to this instance and returns the instance of the {@code Camera} whose lifecycle + * that {@code LifecycleOwner} reflects. + */ + @Override + public Long bindToLifecycle( + @NonNull Long identifier, + @NonNull Long cameraSelectorIdentifier, + @NonNull List useCaseIds) { + ProcessCameraProvider processCameraProvider = + (ProcessCameraProvider) Objects.requireNonNull(instanceManager.getInstance(identifier)); + CameraSelector cameraSelector = + (CameraSelector) + Objects.requireNonNull(instanceManager.getInstance(cameraSelectorIdentifier)); + UseCase[] useCases = new UseCase[useCaseIds.size()]; + for (int i = 0; i < useCaseIds.size(); i++) { + useCases[i] = + (UseCase) + Objects.requireNonNull( + instanceManager.getInstance(((Number) useCaseIds.get(i)).longValue())); + } + + Camera camera = + processCameraProvider.bindToLifecycle( + (LifecycleOwner) lifecycleOwner, cameraSelector, useCases); + + final CameraFlutterApiImpl cameraFlutterApi = + new CameraFlutterApiImpl(binaryMessenger, instanceManager); + if (!instanceManager.containsInstance(camera)) { + cameraFlutterApi.create(camera, result -> {}); + } + + return instanceManager.getIdentifierForStrongReference(camera); + } + + @Override + public void unbind(@NonNull Long identifier, @NonNull List useCaseIds) { + ProcessCameraProvider processCameraProvider = + (ProcessCameraProvider) Objects.requireNonNull(instanceManager.getInstance(identifier)); + UseCase[] useCases = new UseCase[useCaseIds.size()]; + for (int i = 0; i < useCaseIds.size(); i++) { + useCases[i] = + (UseCase) + Objects.requireNonNull( + instanceManager.getInstance(((Number) useCaseIds.get(i)).longValue())); + } + processCameraProvider.unbind(useCases); + } + + @Override + public void unbindAll(@NonNull Long identifier) { + ProcessCameraProvider processCameraProvider = + (ProcessCameraProvider) Objects.requireNonNull(instanceManager.getInstance(identifier)); + processCameraProvider.unbindAll(); + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java new file mode 100644 index 000000000000..63158974f43a --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java @@ -0,0 +1,24 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi; + +public class SystemServicesFlutterApiImpl extends SystemServicesFlutterApi { + public SystemServicesFlutterApiImpl(@NonNull BinaryMessenger binaryMessenger) { + super(binaryMessenger); + } + + public void sendDeviceOrientationChangedEvent( + @NonNull String orientation, @NonNull Reply reply) { + super.onDeviceOrientationChanged(orientation, reply); + } + + public void sendCameraError(@NonNull String errorDescription, @NonNull Reply reply) { + super.onCameraError(errorDescription, reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java new file mode 100644 index 000000000000..a6985811531f --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java @@ -0,0 +1,111 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import android.app.Activity; +import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraPermissionsErrorData; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.Result; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesHostApi; + +public class SystemServicesHostApiImpl implements SystemServicesHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + + @VisibleForTesting public CameraXProxy cameraXProxy = new CameraXProxy(); + @VisibleForTesting public DeviceOrientationManager deviceOrientationManager; + @VisibleForTesting public SystemServicesFlutterApiImpl systemServicesFlutterApi; + + private Activity activity; + private PermissionsRegistry permissionsRegistry; + + public SystemServicesHostApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.systemServicesFlutterApi = new SystemServicesFlutterApiImpl(binaryMessenger); + } + + public void setActivity(Activity activity) { + this.activity = activity; + } + + public void setPermissionsRegistry(PermissionsRegistry permissionsRegistry) { + this.permissionsRegistry = permissionsRegistry; + } + + /** + * Requests camera permissions using an instance of a {@link CameraPermissionsManager}. + * + *

Will result with {@code null} if permissions were approved or there were no errors; + * otherwise, it will result with the error data explaining what went wrong. + */ + @Override + public void requestCameraPermissions( + Boolean enableAudio, Result result) { + CameraPermissionsManager cameraPermissionsManager = + cameraXProxy.createCameraPermissionsManager(); + cameraPermissionsManager.requestPermissions( + activity, + permissionsRegistry, + enableAudio, + (String errorCode, String description) -> { + if (errorCode == null) { + result.success(null); + } else { + // If permissions are ongoing or denied, error data will be sent to be handled. + CameraPermissionsErrorData errorData = + new CameraPermissionsErrorData.Builder() + .setErrorCode(errorCode) + .setDescription(description) + .build(); + result.success(errorData); + } + }); + } + + /** + * Starts listening for device orientation changes using an instace of a {@link + * DeviceOrientationManager}. + * + *

Whenever a change in device orientation is detected by the {@code DeviceOrientationManager}, + * the {@link SystemServicesFlutterApi} will be used to notify the Dart side. + */ + @Override + public void startListeningForDeviceOrientationChange( + Boolean isFrontFacing, Long sensorOrientation) { + deviceOrientationManager = + cameraXProxy.createDeviceOrientationManager( + activity, + isFrontFacing, + sensorOrientation.intValue(), + (DeviceOrientation newOrientation) -> { + systemServicesFlutterApi.sendDeviceOrientationChangedEvent( + serializeDeviceOrientation(newOrientation), reply -> {}); + }); + deviceOrientationManager.start(); + } + + /** Serializes {@code DeviceOrientation} into a String that the Dart side is able to recognize. */ + String serializeDeviceOrientation(DeviceOrientation orientation) { + return orientation.toString(); + } + + /** + * Tells the {@code deviceOrientationManager} to stop listening for orientation updates. + * + *

Has no effect if the {@code deviceOrientationManager} was never created to listen for device + * orientation updates. + */ + @Override + public void stopListeningForDeviceOrientationChange() { + if (deviceOrientationManager != null) { + deviceOrientationManager.stop(); + } + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraPermissionsManagerTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraPermissionsManagerTest.java new file mode 100644 index 000000000000..d90bde953306 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraPermissionsManagerTest.java @@ -0,0 +1,89 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static junit.framework.TestCase.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.pm.PackageManager; +import io.flutter.plugins.camerax.CameraPermissionsManager.CameraRequestPermissionsListener; +import io.flutter.plugins.camerax.CameraPermissionsManager.ResultCallback; +import org.junit.Test; + +public class CameraPermissionsManagerTest { + @Test + public void listener_respondsOnce() { + final int[] calledCounter = {0}; + CameraRequestPermissionsListener permissionsListener = + new CameraRequestPermissionsListener((String code, String desc) -> calledCounter[0]++); + + permissionsListener.onRequestPermissionsResult( + 9796, null, new int[] {PackageManager.PERMISSION_DENIED}); + permissionsListener.onRequestPermissionsResult( + 9796, null, new int[] {PackageManager.PERMISSION_GRANTED}); + + assertEquals(1, calledCounter[0]); + } + + @Test + public void callback_respondsWithCameraAccessDenied() { + ResultCallback fakeResultCallback = mock(ResultCallback.class); + CameraRequestPermissionsListener permissionsListener = + new CameraRequestPermissionsListener(fakeResultCallback); + + permissionsListener.onRequestPermissionsResult( + 9796, null, new int[] {PackageManager.PERMISSION_DENIED}); + + verify(fakeResultCallback) + .onResult("CameraAccessDenied", "Camera access permission was denied."); + } + + @Test + public void callback_respondsWithAudioAccessDenied() { + ResultCallback fakeResultCallback = mock(ResultCallback.class); + CameraRequestPermissionsListener permissionsListener = + new CameraRequestPermissionsListener(fakeResultCallback); + + permissionsListener.onRequestPermissionsResult( + 9796, + null, + new int[] {PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_DENIED}); + + verify(fakeResultCallback).onResult("AudioAccessDenied", "Audio access permission was denied."); + } + + @Test + public void callback_doesNotRespond() { + ResultCallback fakeResultCallback = mock(ResultCallback.class); + CameraRequestPermissionsListener permissionsListener = + new CameraRequestPermissionsListener(fakeResultCallback); + + permissionsListener.onRequestPermissionsResult( + 9796, + null, + new int[] {PackageManager.PERMISSION_GRANTED, PackageManager.PERMISSION_GRANTED}); + + verify(fakeResultCallback, never()) + .onResult("CameraAccessDenied", "Camera access permission was denied."); + verify(fakeResultCallback, never()) + .onResult("AudioAccessDenied", "Audio access permission was denied."); + } + + @Test + public void callback_respondsWithCameraAccessDeniedWhenEmptyResult() { + // Handles the case where the grantResults array is empty + + ResultCallback fakeResultCallback = mock(ResultCallback.class); + CameraRequestPermissionsListener permissionsListener = + new CameraRequestPermissionsListener(fakeResultCallback); + + permissionsListener.onRequestPermissionsResult(9796, null, new int[] {}); + + verify(fakeResultCallback) + .onResult("CameraAccessDenied", "Camera access permission was denied."); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraTest.java new file mode 100644 index 000000000000..e2135b3945b0 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraTest.java @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import androidx.camera.core.Camera; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class CameraTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public Camera camera; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = InstanceManager.open(identifier -> {}); + } + + @After + public void tearDown() { + testInstanceManager.close(); + } + + @Test + public void flutterApiCreateTest() { + final CameraFlutterApiImpl spyFlutterApi = + spy(new CameraFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + + spyFlutterApi.create(camera, reply -> {}); + + final long identifier = + Objects.requireNonNull(testInstanceManager.getIdentifierForStrongReference(camera)); + verify(spyFlutterApi).create(eq(identifier), any()); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java new file mode 100644 index 000000000000..1e2bfba714c7 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java @@ -0,0 +1,313 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.provider.Settings; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugins.camerax.DeviceOrientationManager.DeviceOrientationChangeCallback; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class DeviceOrientationManagerTest { + private Activity mockActivity; + private DeviceOrientationChangeCallback mockDeviceOrientationChangeCallback; + private WindowManager mockWindowManager; + private Display mockDisplay; + private DeviceOrientationManager deviceOrientationManager; + + @Before + @SuppressWarnings("deprecation") + public void before() { + mockActivity = mock(Activity.class); + mockDisplay = mock(Display.class); + mockWindowManager = mock(WindowManager.class); + mockDeviceOrientationChangeCallback = mock(DeviceOrientationChangeCallback.class); + + when(mockActivity.getSystemService(Context.WINDOW_SERVICE)).thenReturn(mockWindowManager); + when(mockWindowManager.getDefaultDisplay()).thenReturn(mockDisplay); + + deviceOrientationManager = + new DeviceOrientationManager(mockActivity, false, 0, mockDeviceOrientationChangeCallback); + } + + @Test + public void getVideoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { + int degreesPortraitUp = + deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(0, degreesPortraitUp); + assertEquals(270, degreesLandscapeLeft); + assertEquals(180, degreesPortraitDown); + assertEquals(90, degreesLandscapeRight); + } + + @Test + public void getVideoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft() { + DeviceOrientationManager orientationManager = + new DeviceOrientationManager(mockActivity, false, 90, mockDeviceOrientationChangeCallback); + + int degreesPortraitUp = orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(90, degreesPortraitUp); + assertEquals(0, degreesLandscapeLeft); + assertEquals(270, degreesPortraitDown); + assertEquals(180, degreesLandscapeRight); + } + + @Test + public void getVideoOrientation_fallbackToPortraitSensorOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + + int degrees = deviceOrientationManager.getVideoOrientation(null); + + assertEquals(0, degrees); + } + + @Test + public void getVideoOrientation_fallbackToLandscapeSensorOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + DeviceOrientationManager orientationManager = + new DeviceOrientationManager(mockActivity, false, 90, mockDeviceOrientationChangeCallback); + + int degrees = orientationManager.getVideoOrientation(null); + + assertEquals(0, degrees); + } + + @Test + public void getPhotoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { + int degreesPortraitUp = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(0, degreesPortraitUp); + assertEquals(90, degreesLandscapeRight); + assertEquals(180, degreesPortraitDown); + assertEquals(270, degreesLandscapeLeft); + } + + @Test + public void getPhotoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft() { + DeviceOrientationManager orientationManager = + new DeviceOrientationManager(mockActivity, false, 90, mockDeviceOrientationChangeCallback); + + int degreesPortraitUp = orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(90, degreesPortraitUp); + assertEquals(180, degreesLandscapeRight); + assertEquals(270, degreesPortraitDown); + assertEquals(0, degreesLandscapeLeft); + } + + @Test + public void getPhotoOrientation_shouldFallbackToCurrentOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + int degrees = deviceOrientationManager.getPhotoOrientation(null); + + assertEquals(270, degrees); + } + + @Test + public void handleUIOrientationChange_shouldSendMessageWhenSensorAccessIsAllowed() { + try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { + mockedSystem + .when( + () -> + Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) + .thenReturn(0); + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + deviceOrientationManager.handleUIOrientationChange(); + } + + verify(mockDeviceOrientationChangeCallback, times(1)) + .onChange(DeviceOrientation.LANDSCAPE_LEFT); + } + + @Test + public void handleOrientationChange_shouldSendMessageWhenOrientationIsUpdated() { + DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; + DeviceOrientation newOrientation = DeviceOrientation.LANDSCAPE_LEFT; + + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDeviceOrientationChangeCallback); + + verify(mockDeviceOrientationChangeCallback, times(1)).onChange(newOrientation); + } + + @Test + public void handleOrientationChange_shouldNotSendMessageWhenOrientationIsNotUpdated() { + DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; + DeviceOrientation newOrientation = DeviceOrientation.PORTRAIT_UP; + + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDeviceOrientationChangeCallback); + + verify(mockDeviceOrientationChangeCallback, never()).onChange(any()); + } + + @Test + public void getUIOrientation() { + // Orientation portrait and rotation of 0 should translate to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + DeviceOrientation uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + + // Orientation portrait and rotation of 90 should translate to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_90); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + + // Orientation portrait and rotation of 180 should translate to "PORTRAIT_DOWN". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_180); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, uiOrientation); + + // Orientation portrait and rotation of 270 should translate to "PORTRAIT_DOWN". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_270); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, uiOrientation); + + // Orientation landscape and rotation of 0 should translate to "LANDSCAPE_LEFT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, uiOrientation); + + // Orientation landscape and rotation of 90 should translate to "LANDSCAPE_LEFT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_90); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, uiOrientation); + + // Orientation landscape and rotation of 180 should translate to "LANDSCAPE_RIGHT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_180); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, uiOrientation); + + // Orientation landscape and rotation of 270 should translate to "LANDSCAPE_RIGHT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_270); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, uiOrientation); + + // Orientation undefined should default to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_UNDEFINED, Surface.ROTATION_0); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + } + + @Test + public void getDeviceDefaultOrientation() { + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + int orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_180); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_90); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_270); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_180); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_90); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_270); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + } + + @Test + public void calculateSensorOrientation() { + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + DeviceOrientation orientation = deviceOrientationManager.calculateSensorOrientation(0); + assertEquals(DeviceOrientation.PORTRAIT_UP, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + orientation = deviceOrientationManager.calculateSensorOrientation(90); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + orientation = deviceOrientationManager.calculateSensorOrientation(180); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + orientation = deviceOrientationManager.calculateSensorOrientation(270); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, orientation); + } + + private void setUpUIOrientationMocks(int orientation, int rotation) { + Resources mockResources = mock(Resources.class); + Configuration mockConfiguration = mock(Configuration.class); + + when(mockDisplay.getRotation()).thenReturn(rotation); + + mockConfiguration.orientation = orientation; + when(mockActivity.getResources()).thenReturn(mockResources); + when(mockResources.getConfiguration()).thenReturn(mockConfiguration); + } + + @Test + public void getDisplayTest() { + Display display = deviceOrientationManager.getDisplay(); + + assertEquals(mockDisplay, display); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/InstanceManagerTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/InstanceManagerTest.java index 3878e05a40e8..e2e012dc35fb 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/InstanceManagerTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/InstanceManagerTest.java @@ -5,6 +5,7 @@ package io.flutter.plugins.camerax; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -40,6 +41,20 @@ public void addHostCreatedInstance() { instanceManager.close(); } + @Test + public void addHostCreatedInstance_createsSameInstanceTwice() { + final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); + + final Object object = new Object(); + long firstIdentifier = instanceManager.addHostCreatedInstance(object); + long secondIdentifier = instanceManager.addHostCreatedInstance(object); + + assertNotEquals(firstIdentifier, secondIdentifier); + assertTrue(instanceManager.containsInstance(object)); + + instanceManager.close(); + } + @Test public void remove() { final InstanceManager instanceManager = InstanceManager.open(identifier -> {}); diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java new file mode 100644 index 000000000000..9cb4e910dbb8 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java @@ -0,0 +1,221 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.SurfaceTexture; +import android.util.Size; +import android.view.Surface; +import androidx.camera.core.Preview; +import androidx.camera.core.SurfaceRequest; +import androidx.core.util.Consumer; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ResolutionInfo; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi.Reply; +import io.flutter.view.TextureRegistry; +import java.util.concurrent.Executor; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class PreviewTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public Preview mockPreview; + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public TextureRegistry mockTextureRegistry; + @Mock public CameraXProxy mockCameraXProxy; + + InstanceManager testInstanceManager; + + @Before + public void setUp() { + testInstanceManager = spy(InstanceManager.open(identifier -> {})); + } + + @After + public void tearDown() { + testInstanceManager.close(); + } + + @Test + public void create_createsPreviewWithCorrectConfiguration() { + final PreviewHostApiImpl previewHostApi = + new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry); + final Preview.Builder mockPreviewBuilder = mock(Preview.Builder.class); + final int targetRotation = 90; + final int targetResolutionWidth = 10; + final int targetResolutionHeight = 50; + final Long previewIdentifier = 3L; + final GeneratedCameraXLibrary.ResolutionInfo resolutionInfo = + new GeneratedCameraXLibrary.ResolutionInfo.Builder() + .setWidth(Long.valueOf(targetResolutionWidth)) + .setHeight(Long.valueOf(targetResolutionHeight)) + .build(); + + previewHostApi.cameraXProxy = mockCameraXProxy; + when(mockCameraXProxy.createPreviewBuilder()).thenReturn(mockPreviewBuilder); + when(mockPreviewBuilder.build()).thenReturn(mockPreview); + + final ArgumentCaptor sizeCaptor = ArgumentCaptor.forClass(Size.class); + + previewHostApi.create(previewIdentifier, Long.valueOf(targetRotation), resolutionInfo); + + verify(mockPreviewBuilder).setTargetRotation(targetRotation); + verify(mockPreviewBuilder).setTargetResolution(sizeCaptor.capture()); + assertEquals(sizeCaptor.getValue().getWidth(), targetResolutionWidth); + assertEquals(sizeCaptor.getValue().getHeight(), targetResolutionHeight); + verify(mockPreviewBuilder).build(); + verify(testInstanceManager).addDartCreatedInstance(mockPreview, previewIdentifier); + } + + @Test + public void setSurfaceProviderTest_createsSurfaceProviderAndReturnsTextureEntryId() { + final PreviewHostApiImpl previewHostApi = + spy(new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry)); + final TextureRegistry.SurfaceTextureEntry mockSurfaceTextureEntry = + mock(TextureRegistry.SurfaceTextureEntry.class); + final SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class); + final Long previewIdentifier = 5L; + final Long surfaceTextureEntryId = 120L; + + previewHostApi.cameraXProxy = mockCameraXProxy; + testInstanceManager.addDartCreatedInstance(mockPreview, previewIdentifier); + + when(mockTextureRegistry.createSurfaceTexture()).thenReturn(mockSurfaceTextureEntry); + when(mockSurfaceTextureEntry.surfaceTexture()).thenReturn(mockSurfaceTexture); + when(mockSurfaceTextureEntry.id()).thenReturn(surfaceTextureEntryId); + + final ArgumentCaptor surfaceProviderCaptor = + ArgumentCaptor.forClass(Preview.SurfaceProvider.class); + final ArgumentCaptor surfaceCaptor = ArgumentCaptor.forClass(Surface.class); + final ArgumentCaptor consumerCaptor = ArgumentCaptor.forClass(Consumer.class); + + // Test that surface provider was set and the surface texture ID was returned. + assertEquals(previewHostApi.setSurfaceProvider(previewIdentifier), surfaceTextureEntryId); + verify(mockPreview).setSurfaceProvider(surfaceProviderCaptor.capture()); + verify(previewHostApi).createSurfaceProvider(mockSurfaceTexture); + } + + @Test + public void createSurfaceProvider_createsExpectedPreviewSurfaceProvider() { + final PreviewHostApiImpl previewHostApi = + new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry); + final SurfaceTexture mockSurfaceTexture = mock(SurfaceTexture.class); + final Surface mockSurface = mock(Surface.class); + final SurfaceRequest mockSurfaceRequest = mock(SurfaceRequest.class); + final SurfaceRequest.Result mockSurfaceRequestResult = mock(SurfaceRequest.Result.class); + final SystemServicesFlutterApiImpl mockSystemServicesFlutterApi = + mock(SystemServicesFlutterApiImpl.class); + final int resolutionWidth = 200; + final int resolutionHeight = 500; + + previewHostApi.cameraXProxy = mockCameraXProxy; + when(mockCameraXProxy.createSurface(mockSurfaceTexture)).thenReturn(mockSurface); + when(mockSurfaceRequest.getResolution()) + .thenReturn(new Size(resolutionWidth, resolutionHeight)); + when(mockCameraXProxy.createSystemServicesFlutterApiImpl(mockBinaryMessenger)) + .thenReturn(mockSystemServicesFlutterApi); + + final ArgumentCaptor surfaceCaptor = ArgumentCaptor.forClass(Surface.class); + final ArgumentCaptor consumerCaptor = ArgumentCaptor.forClass(Consumer.class); + + Preview.SurfaceProvider previewSurfaceProvider = + previewHostApi.createSurfaceProvider(mockSurfaceTexture); + previewSurfaceProvider.onSurfaceRequested(mockSurfaceRequest); + + verify(mockSurfaceTexture).setDefaultBufferSize(resolutionWidth, resolutionHeight); + verify(mockSurfaceRequest) + .provideSurface(surfaceCaptor.capture(), any(Executor.class), consumerCaptor.capture()); + + // Test that the surface derived from the surface texture entry will be provided to the surface request. + assertEquals(surfaceCaptor.getValue(), mockSurface); + + // Test that the Consumer used to handle surface request result releases Flutter surface texture appropriately + // and sends camera errors appropriately. + Consumer capturedConsumer = consumerCaptor.getValue(); + + // Case where Surface should be released. + when(mockSurfaceRequestResult.getResultCode()) + .thenReturn(SurfaceRequest.Result.RESULT_REQUEST_CANCELLED); + capturedConsumer.accept(mockSurfaceRequestResult); + verify(mockSurface).release(); + reset(mockSurface); + + when(mockSurfaceRequestResult.getResultCode()) + .thenReturn(SurfaceRequest.Result.RESULT_REQUEST_CANCELLED); + capturedConsumer.accept(mockSurfaceRequestResult); + verify(mockSurface).release(); + reset(mockSurface); + + when(mockSurfaceRequestResult.getResultCode()) + .thenReturn(SurfaceRequest.Result.RESULT_WILL_NOT_PROVIDE_SURFACE); + capturedConsumer.accept(mockSurfaceRequestResult); + verify(mockSurface).release(); + reset(mockSurface); + + when(mockSurfaceRequestResult.getResultCode()) + .thenReturn(SurfaceRequest.Result.RESULT_SURFACE_USED_SUCCESSFULLY); + capturedConsumer.accept(mockSurfaceRequestResult); + verify(mockSurface).release(); + reset(mockSurface); + + // Case where error must be sent. + when(mockSurfaceRequestResult.getResultCode()) + .thenReturn(SurfaceRequest.Result.RESULT_INVALID_SURFACE); + capturedConsumer.accept(mockSurfaceRequestResult); + verify(mockSurface).release(); + verify(mockSystemServicesFlutterApi).sendCameraError(anyString(), any(Reply.class)); + } + + @Test + public void releaseFlutterSurfaceTexture_makesCallToReleaseFlutterSurfaceTexture() { + final PreviewHostApiImpl previewHostApi = + new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry); + final TextureRegistry.SurfaceTextureEntry mockSurfaceTextureEntry = + mock(TextureRegistry.SurfaceTextureEntry.class); + + previewHostApi.flutterSurfaceTexture = mockSurfaceTextureEntry; + + previewHostApi.releaseFlutterSurfaceTexture(); + verify(mockSurfaceTextureEntry).release(); + } + + @Test + public void getResolutionInfo_makesCallToRetrievePreviewResolutionInfo() { + final PreviewHostApiImpl previewHostApi = + new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry); + final androidx.camera.core.ResolutionInfo mockResolutionInfo = + mock(androidx.camera.core.ResolutionInfo.class); + final Long previewIdentifier = 23L; + final int resolutionWidth = 500; + final int resolutionHeight = 200; + + testInstanceManager.addDartCreatedInstance(mockPreview, previewIdentifier); + when(mockPreview.getResolutionInfo()).thenReturn(mockResolutionInfo); + when(mockResolutionInfo.getResolution()) + .thenReturn(new Size(resolutionWidth, resolutionHeight)); + + ResolutionInfo resolutionInfo = previewHostApi.getResolutionInfo(previewIdentifier); + assertEquals(resolutionInfo.getWidth(), Long.valueOf(resolutionWidth)); + assertEquals(resolutionInfo.getHeight(), Long.valueOf(resolutionHeight)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProcessCameraProviderTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProcessCameraProviderTest.java index 5008e4ef34b0..47b4ed6ad26d 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProcessCameraProviderTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProcessCameraProviderTest.java @@ -13,8 +13,12 @@ import static org.mockito.Mockito.when; import android.content.Context; +import androidx.camera.core.Camera; import androidx.camera.core.CameraInfo; +import androidx.camera.core.CameraSelector; +import androidx.camera.core.UseCase; import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.lifecycle.LifecycleOwner; import androidx.test.core.app.ApplicationProvider; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -99,6 +103,58 @@ public void getAvailableCameraInfosTest() { verify(processCameraProvider).getAvailableCameraInfos(); } + @Test + public void bindToLifecycleTest() { + final ProcessCameraProviderHostApiImpl processCameraProviderHostApi = + new ProcessCameraProviderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + final Camera mockCamera = mock(Camera.class); + final CameraSelector mockCameraSelector = mock(CameraSelector.class); + final UseCase mockUseCase = mock(UseCase.class); + UseCase[] mockUseCases = new UseCase[] {mockUseCase}; + + LifecycleOwner mockLifecycleOwner = mock(LifecycleOwner.class); + processCameraProviderHostApi.setLifecycleOwner(mockLifecycleOwner); + + testInstanceManager.addDartCreatedInstance(processCameraProvider, 0); + testInstanceManager.addDartCreatedInstance(mockCameraSelector, 1); + testInstanceManager.addDartCreatedInstance(mockUseCase, 2); + testInstanceManager.addDartCreatedInstance(mockCamera, 3); + + when(processCameraProvider.bindToLifecycle( + mockLifecycleOwner, mockCameraSelector, mockUseCases)) + .thenReturn(mockCamera); + + assertEquals( + processCameraProviderHostApi.bindToLifecycle(0L, 1L, Arrays.asList(2L)), Long.valueOf(3)); + verify(processCameraProvider) + .bindToLifecycle(mockLifecycleOwner, mockCameraSelector, mockUseCases); + } + + @Test + public void unbindTest() { + final ProcessCameraProviderHostApiImpl processCameraProviderHostApi = + new ProcessCameraProviderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + final UseCase mockUseCase = mock(UseCase.class); + UseCase[] mockUseCases = new UseCase[] {mockUseCase}; + + testInstanceManager.addDartCreatedInstance(processCameraProvider, 0); + testInstanceManager.addDartCreatedInstance(mockUseCase, 1); + + processCameraProviderHostApi.unbind(0L, Arrays.asList(1L)); + verify(processCameraProvider).unbind(mockUseCases); + } + + @Test + public void unbindAllTest() { + final ProcessCameraProviderHostApiImpl processCameraProviderHostApi = + new ProcessCameraProviderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + + testInstanceManager.addDartCreatedInstance(processCameraProvider, 0); + + processCameraProviderHostApi.unbindAll(0L); + verify(processCameraProvider).unbindAll(); + } + @Test public void flutterApiCreateTest() { final ProcessCameraProviderFlutterApiImpl spyFlutterApi = diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java new file mode 100644 index 000000000000..eb36c452ec3b --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java @@ -0,0 +1,138 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; +import io.flutter.plugins.camerax.CameraPermissionsManager.ResultCallback; +import io.flutter.plugins.camerax.DeviceOrientationManager.DeviceOrientationChangeCallback; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraPermissionsErrorData; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.Result; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi.Reply; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class SystemServicesTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public InstanceManager mockInstanceManager; + + @Test + public void requestCameraPermissionsTest() { + final SystemServicesHostApiImpl systemServicesHostApi = + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager); + final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); + final CameraPermissionsManager mockCameraPermissionsManager = + mock(CameraPermissionsManager.class); + final Activity mockActivity = mock(Activity.class); + final PermissionsRegistry mockPermissionsRegistry = mock(PermissionsRegistry.class); + final Result mockResult = mock(Result.class); + final Boolean enableAudio = false; + + systemServicesHostApi.cameraXProxy = mockCameraXProxy; + systemServicesHostApi.setActivity(mockActivity); + systemServicesHostApi.setPermissionsRegistry(mockPermissionsRegistry); + when(mockCameraXProxy.createCameraPermissionsManager()) + .thenReturn(mockCameraPermissionsManager); + + final ArgumentCaptor resultCallbackCaptor = + ArgumentCaptor.forClass(ResultCallback.class); + + systemServicesHostApi.requestCameraPermissions(enableAudio, mockResult); + + // Test camera permissions are requested. + verify(mockCameraPermissionsManager) + .requestPermissions( + eq(mockActivity), + eq(mockPermissionsRegistry), + eq(enableAudio), + resultCallbackCaptor.capture()); + + ResultCallback resultCallback = (ResultCallback) resultCallbackCaptor.getValue(); + + // Test no error data is sent upon permissions request success. + resultCallback.onResult(null, null); + verify(mockResult).success(null); + + // Test expected error data is sent upon permissions request failure. + final String testErrorCode = "TestErrorCode"; + final String testErrorDescription = "Test error description."; + + final ArgumentCaptor cameraPermissionsErrorDataCaptor = + ArgumentCaptor.forClass(CameraPermissionsErrorData.class); + + resultCallback.onResult(testErrorCode, testErrorDescription); + verify(mockResult, times(2)).success(cameraPermissionsErrorDataCaptor.capture()); + + CameraPermissionsErrorData cameraPermissionsErrorData = + cameraPermissionsErrorDataCaptor.getValue(); + assertEquals(cameraPermissionsErrorData.getErrorCode(), testErrorCode); + assertEquals(cameraPermissionsErrorData.getDescription(), testErrorDescription); + } + + @Test + public void deviceOrientationChangeTest() { + final SystemServicesHostApiImpl systemServicesHostApi = + new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager); + final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); + final Activity mockActivity = mock(Activity.class); + final DeviceOrientationManager mockDeviceOrientationManager = + mock(DeviceOrientationManager.class); + final Boolean isFrontFacing = true; + final int sensorOrientation = 90; + + SystemServicesFlutterApiImpl systemServicesFlutterApi = + mock(SystemServicesFlutterApiImpl.class); + systemServicesHostApi.systemServicesFlutterApi = systemServicesFlutterApi; + + systemServicesHostApi.cameraXProxy = mockCameraXProxy; + systemServicesHostApi.setActivity(mockActivity); + when(mockCameraXProxy.createDeviceOrientationManager( + eq(mockActivity), + eq(isFrontFacing), + eq(sensorOrientation), + any(DeviceOrientationChangeCallback.class))) + .thenReturn(mockDeviceOrientationManager); + + final ArgumentCaptor deviceOrientationChangeCallbackCaptor = + ArgumentCaptor.forClass(DeviceOrientationChangeCallback.class); + + systemServicesHostApi.startListeningForDeviceOrientationChange( + isFrontFacing, Long.valueOf(sensorOrientation)); + + // Test callback method defined in Flutter API is called when device orientation changes. + verify(mockCameraXProxy) + .createDeviceOrientationManager( + eq(mockActivity), + eq(isFrontFacing), + eq(sensorOrientation), + deviceOrientationChangeCallbackCaptor.capture()); + DeviceOrientationChangeCallback deviceOrientationChangeCallback = + deviceOrientationChangeCallbackCaptor.getValue(); + + deviceOrientationChangeCallback.onChange(DeviceOrientation.PORTRAIT_DOWN); + verify(systemServicesFlutterApi) + .sendDeviceOrientationChangedEvent( + eq(DeviceOrientation.PORTRAIT_DOWN.toString()), any(Reply.class)); + + // Test that the DeviceOrientationManager starts listening for device orientation changes. + verify(mockDeviceOrientationManager).start(); + } +} diff --git a/packages/camera/camera_android_camerax/example/android/build.gradle b/packages/camera/camera_android_camerax/example/android/build.gradle index 20411f5f31a9..8640e4de86a1 100644 --- a/packages/camera/camera_android_camerax/example/android/build.gradle +++ b/packages/camera/camera_android_camerax/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '1.8.0' repositories { google() mavenCentral() diff --git a/packages/camera/camera_android_camerax/example/android/gradle.properties b/packages/camera/camera_android_camerax/example/android/gradle.properties index 94adc3a3f97a..598d13fee446 100644 --- a/packages/camera/camera_android_camerax/example/android/gradle.properties +++ b/packages/camera/camera_android_camerax/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart b/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart index 2b82b4bda5e4..b05d14a9cc79 100644 --- a/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart +++ b/packages/camera/camera_android_camerax/example/integration_test/integration_test.dart @@ -2,11 +2,27 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:camera_android_camerax/camera_android_camerax.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('placeholder test', (WidgetTester tester) async {}); + setUpAll(() async { + CameraPlatform.instance = AndroidCameraCameraX(); + }); + + testWidgets('availableCameras only supports valid back or front cameras', + (WidgetTester tester) async { + final List availableCameras = + await CameraPlatform.instance.availableCameras(); + + for (final CameraDescription cameraDescription in availableCameras) { + expect( + cameraDescription.lensDirection, isNot(CameraLensDirection.external)); + expect(cameraDescription.sensorOrientation, anyOf(0, 90, 180, 270)); + } + }); } diff --git a/packages/camera/camera_android_camerax/example/lib/camera_controller.dart b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart new file mode 100644 index 000000000000..b1b5e9d4ceb9 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/lib/camera_controller.dart @@ -0,0 +1,957 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:collection'; +import 'dart:math'; + +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 'camera_image.dart'; + +/// Signature for a callback receiving the a camera image. +/// +/// This is used by [CameraController.startImageStream]. +// TODO(stuartmorgan): Fix this naming the next time there's a breaking change +// to this package. +// ignore: camel_case_types +typedef onLatestImageAvailable = Function(CameraImage image); + +/// Completes with a list of available cameras. +/// +/// May throw a [CameraException]. +Future> availableCameras() async { + return CameraPlatform.instance.availableCameras(); +} + +// TODO(stuartmorgan): Remove this once the package requires 2.10, where the +// dart:async `unawaited` accepts a nullable future. +void _unawaited(Future? future) {} + +/// 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, + this.isPreviewPaused = false, + this.previewPauseOrientation, + }) : _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, + isPreviewPaused: 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 the preview widget has been paused manually. + final bool isPreviewPaused; + + /// Set to the orientation the preview was paused in, if it is currently paused. + final DeviceOrientation? previewPauseOrientation; + + /// 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 UI 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, + bool? isPreviewPaused, + Optional? previewPauseOrientation, + }) { + 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, + isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + previewPauseOrientation: previewPauseOrientation == null + ? this.previewPauseOrientation + : previewPauseOrientation.orNull, + ); + } + + @override + String toString() { + return '${objectRuntimeType(this, 'CameraValue')}(' + '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, ' + 'isPreviewPaused: $isPreviewPaused, ' + 'previewPausedOrientation: $previewPauseOrientation)'; + } +} + +/// 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 { + final Completer initializeCompleter = + Completer(); + + _deviceOrientationSubscription = CameraPlatform.instance + .onDeviceOrientationChanged() + .listen((DeviceOrientationChangedEvent event) { + value = value.copyWith( + deviceOrientation: event.orientation, + ); + }); + + _cameraId = await CameraPlatform.instance.createCamera( + description, + resolutionPreset, + enableAudio: enableAudio, + ); + + _unawaited(CameraPlatform.instance + .onCameraInitialized(_cameraId) + .first + .then((CameraInitializedEvent 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((CameraInitializedEvent event) => event.exposureMode), + focusMode: await initializeCompleter.future + .then((CameraInitializedEvent event) => event.focusMode), + exposurePointSupported: await initializeCompleter.future.then( + (CameraInitializedEvent event) => event.exposurePointSupported), + focusPointSupported: await initializeCompleter.future + .then((CameraInitializedEvent 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 and Web. + /// + /// Throws a [CameraException] if the prepare fails. + Future prepareForVideoRecording() async { + await CameraPlatform.instance.prepareForVideoRecording(); + } + + /// Pauses the current camera preview + Future pausePreview() async { + if (value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.pausePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: true, + previewPauseOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resumes the current camera preview + Future resumePreview() async { + if (!value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.resumePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: false, + previewPauseOrientation: const Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// 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); + final 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 { + _imageStreamSubscription = CameraPlatform.instance + .onStreamedFrameAvailable(_cameraId) + .listen((CameraImageData imageData) { + onAvailable(CameraImage.fromPlatformInterface(imageData)); + }); + value = value.copyWith(isStreamingImages: true); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// 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.isStreamingImages) { + throw CameraException( + 'No camera is streaming images', + 'stopImageStream was called when no camera is streaming images.', + ); + } + + try { + value = value.copyWith(isStreamingImages: false); + await _imageStreamSubscription?.cancel(); + _imageStreamSubscription = null; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Start a video recording. + /// + /// You may optionally pass an [onAvailable] callback to also have the + /// video frames streamed to this callback. + /// + /// The video is returned as a [XFile] after calling [stopVideoRecording]. + /// Throws a [CameraException] if the capture fails. + Future startVideoRecording( + {onLatestImageAvailable? onAvailable}) async { + _throwIfNotInitialized('startVideoRecording'); + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'startVideoRecording was called when a recording is already started.', + ); + } + + Function(CameraImageData image)? streamCallback; + if (onAvailable != null) { + streamCallback = (CameraImageData imageData) { + onAvailable(CameraImage.fromPlatformInterface(imageData)); + }; + } + + try { + await CameraPlatform.instance.startVideoCapturing( + VideoCaptureOptions(_cameraId, streamCallback: streamCallback)); + value = value.copyWith( + isRecordingVideo: true, + isRecordingPaused: false, + recordingOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation), + isStreamingImages: onAvailable != null); + } 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.', + ); + } + + if (value.isStreamingImages) { + stopImageStream(); + } + + try { + final XFile file = + await CameraPlatform.instance.stopVideoRecording(_cameraId); + value = value.copyWith( + isRecordingVideo: false, + recordingOrientation: const 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 + final 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 + final double stepSize = await getExposureOffsetStepSize(); + if (stepSize > 0) { + final 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.of( + 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: const 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.', + ); + } + } + + @override + void removeListener(VoidCallback listener) { + // Prevent ValueListenableBuilder in CameraPreview widget from causing an + // exception to be thrown by attempting to remove its own listener after + // the controller has already been disposed. + if (!_isDisposed) { + super.removeListener(listener); + } + } +} + +/// A value that might be absent. +/// +/// Used to represent [DeviceOrientation]s that are optional but also able +/// to be cleared. +@immutable +class Optional extends IterableBase { + /// Constructs an empty Optional. + const Optional.absent() : _value = null; + + /// Constructs an Optional of the given [value]. + /// + /// Throws [ArgumentError] if [value] is null. + Optional.of(T value) : _value = value { + // TODO(cbracken): Delete and make this ctor const once mixed-mode + // execution is no longer around. + ArgumentError.checkNotNull(value); + } + + /// Constructs an Optional of the given [value]. + /// + /// If [value] is null, returns [absent()]. + const Optional.fromNullable(T? value) : _value = value; + + final T? _value; + + /// True when this optional contains a value. + bool get isPresent => _value != null; + + /// True when this optional contains no value. + bool get isNotPresent => _value == null; + + /// Gets the Optional value. + /// + /// Throws [StateError] if [value] is null. + T get value { + if (_value == null) { + throw StateError('value called on absent Optional.'); + } + return _value!; + } + + /// Executes a function if the Optional value is present. + void ifPresent(void Function(T value) ifPresent) { + if (isPresent) { + ifPresent(_value as T); + } + } + + /// Execution a function if the Optional value is absent. + void ifAbsent(void Function() ifAbsent) { + if (!isPresent) { + ifAbsent(); + } + } + + /// Gets the Optional value with a default. + /// + /// The default is returned if the Optional is [absent()]. + /// + /// Throws [ArgumentError] if [defaultValue] is null. + T or(T defaultValue) { + return _value ?? defaultValue; + } + + /// Gets the Optional value, or `null` if there is none. + T? get orNull => _value; + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// The transformer must not return `null`. If it does, an [ArgumentError] is thrown. + Optional transform(S Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.of(transformer(_value as T)); + } + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// Returns [absent()] if the transformer returns `null`. + Optional transformNullable(S? Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.fromNullable(transformer(_value as T)); + } + + @override + Iterator get iterator => + isPresent ? [_value as T].iterator : Iterable.empty().iterator; + + /// Delegates to the underlying [value] hashCode. + @override + int get hashCode => _value.hashCode; + + /// Delegates to the underlying [value] operator==. + @override + bool operator ==(Object o) => o is Optional && o._value == _value; + + @override + String toString() { + return _value == null + ? 'Optional { absent }' + : 'Optional { value: $_value }'; + } +} diff --git a/packages/camera/camera_android_camerax/example/lib/camera_image.dart b/packages/camera/camera_android_camerax/example/lib/camera_image.dart new file mode 100644 index 000000000000..bfcad6626dd6 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/lib/camera_image.dart @@ -0,0 +1,177 @@ +// 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. + +// TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) +// ignore: unnecessary_import +import 'dart:typed_data'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; + +// TODO(stuartmorgan): Remove all of these classes in a breaking change, and +// vend the platform interface versions directly. See +// https://github.com/flutter/flutter/issues/104188 + +/// A single color plane of image data. +/// +/// The number and meaning of the planes in an image are determined by the +/// format of the Image. +class Plane { + Plane._fromPlatformInterface(CameraImagePlane plane) + : bytes = plane.bytes, + bytesPerPixel = plane.bytesPerPixel, + bytesPerRow = plane.bytesPerRow, + height = plane.height, + width = plane.width; + + // Only used by the deprecated codepath that's kept to avoid breaking changes. + // Never called by the plugin itself. + Plane._fromPlatformData(Map data) + : bytes = data['bytes'] as Uint8List, + bytesPerPixel = data['bytesPerPixel'] as int?, + bytesPerRow = data['bytesPerRow'] as int, + height = data['height'] as int?, + width = data['width'] as int?; + + /// Bytes representing this plane. + final Uint8List bytes; + + /// The distance between adjacent pixel samples on Android, in bytes. + /// + /// Will be `null` on iOS. + final int? bytesPerPixel; + + /// The row stride for this color plane, in bytes. + final int bytesPerRow; + + /// Height of the pixel buffer on iOS. + /// + /// Will be `null` on Android + final int? height; + + /// Width of the pixel buffer on iOS. + /// + /// Will be `null` on Android. + final int? width; +} + +/// Describes how pixels are represented in an image. +class ImageFormat { + ImageFormat._fromPlatformInterface(CameraImageFormat format) + : group = format.group, + raw = format.raw; + + // Only used by the deprecated codepath that's kept to avoid breaking changes. + // Never called by the plugin itself. + ImageFormat._fromPlatformData(this.raw) : group = _asImageFormatGroup(raw); + + /// Describes the format group the raw image format falls into. + final ImageFormatGroup group; + + /// Raw version of the format from the Android or iOS platform. + /// + /// On Android, this is an `int` from class `android.graphics.ImageFormat`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat + /// + /// On iOS, this is a `FourCharCode` constant from Pixel Format Identifiers. + /// See https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers?language=objc + final dynamic raw; +} + +// Only used by the deprecated codepath that's kept to avoid breaking changes. +// Never called by the plugin itself. +ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { + if (defaultTargetPlatform == TargetPlatform.android) { + switch (rawFormat) { + // android.graphics.ImageFormat.YUV_420_888 + case 35: + return ImageFormatGroup.yuv420; + // android.graphics.ImageFormat.JPEG + case 256: + return ImageFormatGroup.jpeg; + } + } + + if (defaultTargetPlatform == TargetPlatform.iOS) { + switch (rawFormat) { + // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + case 875704438: + return ImageFormatGroup.yuv420; + // kCVPixelFormatType_32BGRA + case 1111970369: + return ImageFormatGroup.bgra8888; + } + } + + return ImageFormatGroup.unknown; +} + +/// A single complete image buffer from the platform camera. +/// +/// This class allows for direct application access to the pixel data of an +/// Image through one or more [Uint8List]. Each buffer is encapsulated in a +/// [Plane] that describes the layout of the pixel data in that plane. The +/// [CameraImage] is not directly usable as a UI resource. +/// +/// Although not all image formats are planar on iOS, we treat 1-dimensional +/// images as single planar images. +class CameraImage { + /// Creates a [CameraImage] from the platform interface version. + CameraImage.fromPlatformInterface(CameraImageData data) + : format = ImageFormat._fromPlatformInterface(data.format), + height = data.height, + width = data.width, + planes = List.unmodifiable(data.planes.map( + (CameraImagePlane plane) => Plane._fromPlatformInterface(plane))), + lensAperture = data.lensAperture, + sensorExposureTime = data.sensorExposureTime, + sensorSensitivity = data.sensorSensitivity; + + /// Creates a [CameraImage] from method channel data. + @Deprecated('Use fromPlatformInterface instead') + CameraImage.fromPlatformData(Map data) + : format = ImageFormat._fromPlatformData(data['format']), + height = data['height'] as int, + width = data['width'] as int, + lensAperture = data['lensAperture'] as double?, + sensorExposureTime = data['sensorExposureTime'] as int?, + sensorSensitivity = data['sensorSensitivity'] as double?, + planes = List.unmodifiable((data['planes'] as List) + .map((dynamic planeData) => + Plane._fromPlatformData(planeData as Map))); + + /// Format of the image provided. + /// + /// Determines the number of planes needed to represent the image, and + /// the general layout of the pixel data in each [Uint8List]. + final ImageFormat format; + + /// Height of the image in pixels. + /// + /// For formats where some color channels are subsampled, this is the height + /// of the largest-resolution plane. + final int height; + + /// Width of the image in pixels. + /// + /// For formats where some color channels are subsampled, this is the width + /// of the largest-resolution plane. + final int width; + + /// The pixels planes for this image. + /// + /// The number of planes is determined by the format of the image. + final List planes; + + /// The aperture settings for this image. + /// + /// Represented as an f-stop value. + final double? lensAperture; + + /// The sensor exposure time for this image in nanoseconds. + final int? sensorExposureTime; + + /// The sensor sensitivity in standard ISO arithmetic units. + final double? sensorSensitivity; +} diff --git a/packages/camera/camera_android_camerax/example/lib/camera_preview.dart b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart new file mode 100644 index 000000000000..3baaaf8b1fa1 --- /dev/null +++ b/packages/camera/camera_android_camerax/example/lib/camera_preview.dart @@ -0,0 +1,81 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'camera_controller.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, {super.key, 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 + ? ValueListenableBuilder( + valueListenable: controller, + builder: (BuildContext context, Object? value, Widget? child) { + return AspectRatio( + aspectRatio: _isLandscape() + ? controller.value.aspectRatio + : (1 / controller.value.aspectRatio), + child: Stack( + fit: StackFit.expand, + children: [ + _wrapInRotatedBox(child: controller.buildPreview()), + child ?? Container(), + ], + ), + ); + }, + child: child, + ) + : Container(); + } + + Widget _wrapInRotatedBox({required Widget child}) { + if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) { + return child; + } + + return RotatedBox( + quarterTurns: _getQuarterTurns(), + child: child, + ); + } + + bool _isLandscape() { + return [ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight + ].contains(_getApplicableOrientation()); + } + + int _getQuarterTurns() { + final Map turns = { + DeviceOrientation.portraitUp: 0, + DeviceOrientation.landscapeRight: 1, + DeviceOrientation.portraitDown: 2, + DeviceOrientation.landscapeLeft: 3, + }; + return turns[_getApplicableOrientation()]!; + } + + DeviceOrientation _getApplicableOrientation() { + return controller.value.isRecordingVideo + ? controller.value.recordingOrientation! + : (controller.value.previewPauseOrientation ?? + controller.value.lockedCaptureOrientation ?? + controller.value.deviceOrientation); + } +} diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index 244a15281e3f..4fd965271baa 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -2,43 +2,1046 @@ // 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'; + import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:video_player/video_player.dart'; -late List _cameras; +import 'camera_controller.dart'; +import 'camera_preview.dart'; -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - _cameras = await CameraPlatform.instance.availableCameras(); +/// Camera example home widget. +class CameraExampleHome extends StatefulWidget { + /// Default Constructor + const CameraExampleHome({super.key}); - runApp(const MyApp()); + @override + State createState() { + return _CameraExampleHomeState(); + } } -/// Example app -class MyApp extends StatefulWidget { - /// App instantiation - const MyApp({super.key}); - @override - State createState() => _MyAppState(); +/// Returns a suitable camera icon for [direction]. +IconData getCameraLensIcon(CameraLensDirection direction) { + switch (direction) { + case CameraLensDirection.back: + return Icons.camera_rear; + case CameraLensDirection.front: + return Icons.camera_front; + case CameraLensDirection.external: + return Icons.camera; + } + // This enum is from a different package, so a new value could be added at + // any time. The example should keep working if that happens. + // ignore: dead_code + return Icons.camera; +} + +void _logError(String code, String? message) { + // ignore: avoid_print + print('Error: $code${message == null ? '' : '\nError Message: $message'}'); } -class _MyAppState extends State { +class _CameraExampleHomeState extends State + with WidgetsBindingObserver, TickerProviderStateMixin { + CameraController? controller; + XFile? imageFile; + XFile? videoFile; + VideoPlayerController? videoController; + VoidCallback? videoPlayerListener; + bool enableAudio = true; + double _minAvailableExposureOffset = 0.0; + double _maxAvailableExposureOffset = 0.0; + double _currentExposureOffset = 0.0; + late AnimationController _flashModeControlRowAnimationController; + late Animation _flashModeControlRowAnimation; + late AnimationController _exposureModeControlRowAnimationController; + late Animation _exposureModeControlRowAnimation; + late AnimationController _focusModeControlRowAnimationController; + late Animation _focusModeControlRowAnimation; + double _minAvailableZoom = 1.0; + double _maxAvailableZoom = 1.0; + 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(); + } + + // #docregion AppLifecycle + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final CameraController? cameraController = controller; + + // App state changed before we got the chance to initialize. + if (cameraController == null || !cameraController.value.isInitialized) { + return; + } + + if (state == AppLifecycleState.inactive) { + cameraController.dispose(); + } else if (state == AppLifecycleState.resumed) { + onNewCameraSelected(cameraController.description); + } + } + // #enddocregion AppLifecycle + @override Widget build(BuildContext context) { - String availableCameraNames = 'Available cameras:'; - for (final CameraDescription cameraDescription in _cameras) { - availableCameraNames = '$availableCameraNames ${cameraDescription.name},'; + return Scaffold( + appBar: AppBar( + title: const Text('Camera example'), + ), + body: Column( + children: [ + Expanded( + child: Container( + decoration: BoxDecoration( + color: Colors.black, + border: Border.all( + color: + controller != null && controller!.value.isRecordingVideo + ? Colors.redAccent + : Colors.grey, + width: 3.0, + ), + ), + child: Padding( + padding: const EdgeInsets.all(1.0), + child: Center( + child: _cameraPreviewWidget(), + ), + ), + ), + ), + _captureControlRowWidget(), + _modeControlRowWidget(), + Padding( + padding: const EdgeInsets.all(5.0), + child: Row( + children: [ + _cameraTogglesRowWidget(), + _thumbnailWidget(), + ], + ), + ), + ], + ), + ); + } + + /// Display the preview from the camera (or a message if the preview is not available). + Widget _cameraPreviewWidget() { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + return const Text( + 'Tap a camera', + style: TextStyle( + color: Colors.white, + fontSize: 24.0, + fontWeight: FontWeight.w900, + ), + ); + } else { + 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: (TapDownDetails details) => + onViewFinderTap(details, constraints), + ); + }), + ), + ); + } + } + + void _handleScaleStart(ScaleStartDetails details) { + _baseScale = _currentScale; + } + + Future _handleScaleUpdate(ScaleUpdateDetails details) async { + // When there are not exactly two fingers on screen don't scale + if (controller == null || _pointers != 2) { + return; } - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Camera Example'), + + _currentScale = (_baseScale * details.scale) + .clamp(_minAvailableZoom, _maxAvailableZoom); + + await controller!.setZoomLevel(_currentScale); + } + + /// Display the thumbnail of the captured image or video. + Widget _thumbnailWidget() { + final VideoPlayerController? localVideoController = videoController; + + return Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (localVideoController == null && imageFile == null) + Container() + else + SizedBox( + width: 64.0, + height: 64.0, + child: (localVideoController == null) + ? ( + // The captured image on the web contains a network-accessible URL + // pointing to a location within the browser. It may be displayed + // either with Image.network or Image.memory after loading the image + // bytes to memory. + kIsWeb + ? Image.network(imageFile!.path) + : Image.file(File(imageFile!.path))) + : Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.pink)), + child: Center( + child: AspectRatio( + aspectRatio: + localVideoController.value.size != null + ? localVideoController.value.aspectRatio + : 1.0, + child: VideoPlayer(localVideoController)), + ), + ), + ), + ], + ), + ), + ); + } + + /// Display a bar with buttons to change the flash and exposure modes + Widget _modeControlRowWidget() { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.flash_on), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + // The exposure and focus mode are currently not supported on the web. + ...!kIsWeb + ? [ + IconButton( + icon: const Icon(Icons.exposure), + color: Colors.blue, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.filter_center_focus), + color: Colors.blue, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + ) + ] + : [], + IconButton( + icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: Icon(controller?.value.isCaptureOrientationLocked ?? false + ? Icons.screen_lock_rotation + : Icons.screen_rotation), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + ], ), - body: Center( - child: Text(availableCameraNames.substring( - 0, availableCameraNames.length - 1)), + _flashModeControlRowWidget(), + _exposureModeControlRowWidget(), + _focusModeControlRowWidget(), + ], + ); + } + + Widget _flashModeControlRowWidget() { + return SizeTransition( + sizeFactor: _flashModeControlRowAnimation, + child: ClipRect( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.flash_off), + color: controller?.value.flashMode == FlashMode.off + ? Colors.orange + : Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.flash_auto), + color: controller?.value.flashMode == FlashMode.auto + ? Colors.orange + : Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.flash_on), + color: controller?.value.flashMode == FlashMode.always + ? Colors.orange + : Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.highlight), + color: controller?.value.flashMode == FlashMode.torch + ? Colors.orange + : Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + ], ), ), ); } + + Widget _exposureModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: controller?.value.exposureMode == ExposureMode.auto + ? Colors.orange + : Colors.blue, + ); + final ButtonStyle styleLocked = TextButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + 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: [ + const Center( + child: Text('Exposure Mode'), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + style: styleAuto, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + onLongPress: () { + if (controller != null) { + controller!.setExposurePoint(null); + showInSnackBar('Resetting exposure point'); + } + }, + child: const Text('AUTO'), + ), + TextButton( + style: styleLocked, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + child: const Text('LOCKED'), + ), + TextButton( + style: styleLocked, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + child: const Text('RESET OFFSET'), + ), + ], + ), + const Center( + child: Text('Exposure Offset'), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + 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( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: controller?.value.focusMode == FocusMode.auto + ? Colors.orange + : Colors.blue, + ); + final ButtonStyle styleLocked = TextButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + 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: [ + const Center( + child: Text('Focus Mode'), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + TextButton( + style: styleAuto, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + onLongPress: () { + if (controller != null) { + controller!.setFocusPoint(null); + } + showInSnackBar('Resetting focus point'); + }, + child: const Text('AUTO'), + ), + TextButton( + style: styleLocked, + onPressed: + () {}, // TODO(camsim99): Add functionality back here. + child: const Text('LOCKED'), + ), + ], + ), + ], + ), + ), + ), + ); + } + + /// Display the control bar with buttons to take pictures and record videos. + Widget _captureControlRowWidget() { + final CameraController? cameraController = controller; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.camera_alt), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.videocam), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: cameraController != null && + cameraController.value.isRecordingPaused + ? const Icon(Icons.play_arrow) + : const Icon(Icons.pause), + color: Colors.blue, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.stop), + color: Colors.red, + onPressed: () {}, // TODO(camsim99): Add functionality back here. + ), + IconButton( + icon: const Icon(Icons.pause_presentation), + color: + cameraController != null && cameraController.value.isPreviewPaused + ? Colors.red + : Colors.blue, + onPressed: + cameraController == null ? null : onPausePreviewButtonPressed, + ), + ], + ); + } + + /// Display a row of toggle to select the camera (or a message if no camera is available). + Widget _cameraTogglesRowWidget() { + final List toggles = []; + + void onChanged(CameraDescription? description) { + if (description == null) { + return; + } + + onNewCameraSelected(description); + } + + if (_cameras.isEmpty) { + SchedulerBinding.instance.addPostFrameCallback((_) async { + showInSnackBar('No camera found.'); + }); + return const Text('None'); + } else { + for (final CameraDescription cameraDescription in _cameras) { + toggles.add( + SizedBox( + width: 90.0, + child: RadioListTile( + title: Icon(getCameraLensIcon(cameraDescription.lensDirection)), + groupValue: controller?.description, + value: cameraDescription, + onChanged: + controller != null && controller!.value.isRecordingVideo + ? null + : onChanged, + ), + ), + ); + } + } + + return Row(children: toggles); + } + + String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); + + void showInSnackBar(String message) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(message))); + } + + void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { + if (controller == null) { + return; + } + + final CameraController cameraController = controller!; + + final Offset offset = Offset( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ); + cameraController.setExposurePoint(offset); + cameraController.setFocusPoint(offset); + } + + Future onNewCameraSelected(CameraDescription cameraDescription) async { + final CameraController? oldController = controller; + if (oldController != null) { + // `controller` needs to be set to null before getting disposed, + // to avoid a race condition when we use the controller that is being + // disposed. This happens when camera permission dialog shows up, + // which triggers `didChangeAppLifecycleState`, which disposes and + // re-creates the controller. + controller = null; + await oldController.dispose(); + } + + final CameraController cameraController = CameraController( + cameraDescription, + kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, + enableAudio: enableAudio, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + + controller = cameraController; + + // If the controller is updated then update the UI. + cameraController.addListener(() { + if (mounted) { + setState(() {}); + } + if (cameraController.value.hasError) { + showInSnackBar( + 'Camera error ${cameraController.value.errorDescription}'); + } + }); + + try { + await cameraController.initialize(); + await Future.wait(>[ + // The exposure mode is currently not supported on the web. + ...!kIsWeb + ? >[ + cameraController.getMinExposureOffset().then( + (double value) => _minAvailableExposureOffset = value), + cameraController + .getMaxExposureOffset() + .then((double value) => _maxAvailableExposureOffset = value) + ] + : >[], + cameraController + .getMaxZoomLevel() + .then((double value) => _maxAvailableZoom = value), + cameraController + .getMinZoomLevel() + .then((double value) => _minAvailableZoom = value), + ]); + } on CameraException catch (e) { + switch (e.code) { + case 'CameraAccessDenied': + showInSnackBar('You have denied camera access.'); + break; + case 'CameraAccessDeniedWithoutPrompt': + // iOS only + showInSnackBar('Please go to Settings app to enable camera access.'); + break; + case 'CameraAccessRestricted': + // iOS only + showInSnackBar('Camera access is restricted.'); + break; + case 'AudioAccessDenied': + showInSnackBar('You have denied audio access.'); + break; + case 'AudioAccessDeniedWithoutPrompt': + // iOS only + showInSnackBar('Please go to Settings app to enable audio access.'); + break; + case 'AudioAccessRestricted': + // iOS only + showInSnackBar('Audio access is restricted.'); + break; + default: + _showCameraException(e); + break; + } + } + + if (mounted) { + setState(() {}); + } + } + + void onTakePictureButtonPressed() { + takePicture().then((XFile? file) { + if (mounted) { + setState(() { + imageFile = file; + videoController?.dispose(); + videoController = null; + }); + 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); + } + } + + Future onCaptureOrientationLockButtonPressed() async { + try { + if (controller != null) { + final CameraController cameraController = controller!; + if (cameraController.value.isCaptureOrientationLocked) { + await cameraController.unlockCaptureOrientation(); + showInSnackBar('Capture orientation unlocked'); + } else { + await cameraController.lockCaptureOrientation(); + showInSnackBar( + 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); + } + } + } on CameraException catch (e) { + _showCameraException(e); + } + } + + 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((_) { + if (mounted) { + setState(() {}); + } + }); + } + + void onStopButtonPressed() { + stopVideoRecording().then((XFile? file) { + if (mounted) { + setState(() {}); + } + if (file != null) { + showInSnackBar('Video recorded to ${file.path}'); + videoFile = file; + _startVideoPlayer(); + } + }); + } + + Future onPausePreviewButtonPressed() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isPreviewPaused) { + await cameraController.resumePreview(); + } else { + await cameraController.pausePreview(); + } + + if (mounted) { + setState(() {}); + } + } + + void onPauseButtonPressed() { + pauseVideoRecording().then((_) { + if (mounted) { + setState(() {}); + } + showInSnackBar('Video recording paused'); + }); + } + + void onResumeButtonPressed() { + resumeVideoRecording().then((_) { + if (mounted) { + setState(() {}); + } + showInSnackBar('Video recording resumed'); + }); + } + + Future startVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isRecordingVideo) { + // A recording is already started, do nothing. + return; + } + + try { + await cameraController.startVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return; + } + } + + Future stopVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return null; + } + + try { + return cameraController.stopVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + Future pauseVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return; + } + + try { + await cameraController.pauseVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future resumeVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return; + } + + try { + await cameraController.resumeVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setFlashMode(FlashMode mode) async { + if (controller == null) { + return; + } + + try { + await controller!.setFlashMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setExposureMode(ExposureMode mode) async { + if (controller == null) { + return; + } + + try { + await controller!.setExposureMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setExposureOffset(double offset) async { + if (controller == null) { + return; + } + + setState(() { + _currentExposureOffset = offset; + }); + try { + offset = await controller!.setExposureOffset(offset); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future setFocusMode(FocusMode mode) async { + if (controller == null) { + return; + } + + try { + await controller!.setFocusMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + + Future _startVideoPlayer() async { + if (videoFile == null) { + return; + } + + final VideoPlayerController vController = kIsWeb + ? VideoPlayerController.network(videoFile!.path) + : VideoPlayerController.file(File(videoFile!.path)); + + videoPlayerListener = () { + if (videoController != null && videoController!.value.size != null) { + // Refreshing the state to update video player with the correct ratio. + if (mounted) { + setState(() {}); + } + videoController!.removeListener(videoPlayerListener!); + } + }; + vController.addListener(videoPlayerListener!); + await vController.setLooping(true); + await vController.initialize(); + await videoController?.dispose(); + if (mounted) { + setState(() { + imageFile = null; + videoController = vController; + }); + } + await vController.play(); + } + + Future takePicture() async { + final CameraController? cameraController = controller; + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return null; + } + + if (cameraController.value.isTakingPicture) { + // A capture is already pending, do nothing. + return null; + } + + try { + final XFile file = await cameraController.takePicture(); + return file; + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + void _showCameraException(CameraException e) { + _logError(e.code, e.description); + showInSnackBar('Error: ${e.code}\n${e.description}'); + } +} + +/// CameraApp is the Main Application. +class CameraApp extends StatelessWidget { + /// Default Constructor + const CameraApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: CameraExampleHome(), + ); + } +} + +List _cameras = []; + +Future main() async { + // Fetch the available cameras before initializing the app. + try { + WidgetsFlutterBinding.ensureInitialized(); + _cameras = await availableCameras(); + } on CameraException catch (e) { + _logError(e.code, e.description); + } + runApp(const CameraApp()); } diff --git a/packages/camera/camera_android_camerax/example/pubspec.yaml b/packages/camera/camera_android_camerax/example/pubspec.yaml index d9756f7ebd9b..49a29b8517d9 100644 --- a/packages/camera/camera_android_camerax/example/pubspec.yaml +++ b/packages/camera/camera_android_camerax/example/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: camera_platform_interface: ^2.2.0 flutter: sdk: flutter + video_player: ^2.4.10 dev_dependencies: flutter_test: diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index f03273861793..18debf688547 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -5,6 +5,18 @@ import 'dart:async'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_transform/stream_transform.dart'; + +import 'camera.dart'; +import 'camera_info.dart'; +import 'camera_selector.dart'; +import 'camerax_library.g.dart'; +import 'preview.dart'; +import 'process_camera_provider.dart'; +import 'surface.dart'; +import 'system_services.dart'; +import 'use_case.dart'; /// The Android implementation of [CameraPlatform] that uses the CameraX library. class AndroidCameraCameraX extends CameraPlatform { @@ -13,9 +25,358 @@ class AndroidCameraCameraX extends CameraPlatform { CameraPlatform.instance = AndroidCameraCameraX(); } + /// The [ProcessCameraProvider] instance used to access camera functionality. + @visibleForTesting + ProcessCameraProvider? processCameraProvider; + + /// The [Camera] instance returned by the [processCameraProvider] when a [UseCase] is + /// bound to the lifecycle of the camera it manages. + @visibleForTesting + Camera? camera; + + /// The [Preview] instance that can be configured to present a live camera preview. + @visibleForTesting + Preview? preview; + + /// Whether or not the [preview] is currently bound to the lifecycle that the + /// [processCameraProvider] tracks. + @visibleForTesting + bool previewIsBound = false; + + bool _previewIsPaused = false; + + /// The [CameraSelector] used to configure the [processCameraProvider] to use + /// the desired camera. + @visibleForTesting + CameraSelector? cameraSelector; + + /// The controller we need to broadcast the different 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 stream of camera events. + Stream _cameraEvents(int cameraId) => + cameraEventStreamController.stream + .where((CameraEvent event) => event.cameraId == cameraId); + /// Returns list of all available cameras and their descriptions. @override Future> availableCameras() async { - throw UnimplementedError('availableCameras() is not implemented.'); + final List cameraDescriptions = []; + + processCameraProvider ??= await ProcessCameraProvider.getInstance(); + final List cameraInfos = + await processCameraProvider!.getAvailableCameraInfos(); + + CameraLensDirection? cameraLensDirection; + int cameraCount = 0; + int? cameraSensorOrientation; + String? cameraName; + + for (final CameraInfo cameraInfo in cameraInfos) { + // Determine the lens direction by filtering the CameraInfo + // TODO(gmackall): replace this with call to CameraInfo.getLensFacing when changes containing that method are available + if ((await createCameraSelector(CameraSelector.lensFacingBack) + .filter([cameraInfo])) + .isNotEmpty) { + cameraLensDirection = CameraLensDirection.back; + } else if ((await createCameraSelector(CameraSelector.lensFacingFront) + .filter([cameraInfo])) + .isNotEmpty) { + cameraLensDirection = CameraLensDirection.front; + } else { + //Skip this CameraInfo as its lens direction is unknown + continue; + } + + cameraSensorOrientation = await cameraInfo.getSensorRotationDegrees(); + cameraName = 'Camera $cameraCount'; + cameraCount++; + + cameraDescriptions.add(CameraDescription( + name: cameraName, + lensDirection: cameraLensDirection, + sensorOrientation: cameraSensorOrientation)); + } + + return cameraDescriptions; + } + + /// Creates an uninitialized camera instance and returns the camera ID. + /// + /// In the CameraX library, cameras are accessed by combining [UseCase]s + /// to an instance of a [ProcessCameraProvider]. Thus, to create an + /// unitialized camera instance, this method retrieves a + /// [ProcessCameraProvider] instance. + /// + /// To return the camera ID, which is equivalent to the ID of the surface texture + /// that a camera preview can be drawn to, a [Preview] instance is configured + /// and bound to the [ProcessCameraProvider] instance. + @override + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset? resolutionPreset, { + bool enableAudio = false, + }) async { + // Must obtain proper permissions before attempting to access a camera. + await requestCameraPermissions(enableAudio); + + // Save CameraSelector that matches cameraDescription. + final int cameraSelectorLensDirection = + _getCameraSelectorLensDirection(cameraDescription.lensDirection); + final bool cameraIsFrontFacing = + cameraSelectorLensDirection == CameraSelector.lensFacingFront; + cameraSelector = createCameraSelector(cameraSelectorLensDirection); + // Start listening for device orientation changes preceding camera creation. + startListeningForDeviceOrientationChange( + cameraIsFrontFacing, cameraDescription.sensorOrientation); + + // Retrieve a ProcessCameraProvider instance. + processCameraProvider ??= await ProcessCameraProvider.getInstance(); + + // Configure Preview instance and bind to ProcessCameraProvider. + final int targetRotation = + _getTargetRotation(cameraDescription.sensorOrientation); + final ResolutionInfo? targetResolution = + _getTargetResolutionForPreview(resolutionPreset); + preview = createPreview(targetRotation, targetResolution); + previewIsBound = false; + _previewIsPaused = false; + final int flutterSurfaceTextureId = await preview!.setSurfaceProvider(); + + return flutterSurfaceTextureId; + } + + /// Initializes the camera on the device. + /// + /// Since initialization of a camera does not directly map as an operation to + /// the CameraX library, this method just retrieves information about the + /// camera and sends a [CameraInitializedEvent]. + /// + /// [imageFormatGroup] is used to specify the image formatting used. + /// On Android this defaults to ImageFormat.YUV_420_888 and applies only to + /// the image stream. + @override + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) async { + // TODO(camsim99): Use imageFormatGroup to configure ImageAnalysis use case + // for image streaming. + // https://github.com/flutter/flutter/issues/120463 + + // Configure CameraInitializedEvent to send as representation of a + // configured camera: + // Retrieve preview resolution. + assert( + preview != null, + 'Preview instance not found. Please call the "createCamera" method before calling "initializeCamera"', + ); + await _bindPreviewToLifecycle(); + final ResolutionInfo previewResolutionInfo = + await preview!.getResolutionInfo(); + _unbindPreviewFromLifecycle(); + + // Retrieve exposure and focus mode configurations: + // TODO(camsim99): Implement support for retrieving exposure mode configuration. + // https://github.com/flutter/flutter/issues/120468 + const ExposureMode exposureMode = ExposureMode.auto; + const bool exposurePointSupported = false; + + // TODO(camsim99): Implement support for retrieving focus mode configuration. + // https://github.com/flutter/flutter/issues/120467 + const FocusMode focusMode = FocusMode.auto; + const bool focusPointSupported = false; + + cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + previewResolutionInfo.width.toDouble(), + previewResolutionInfo.height.toDouble(), + exposureMode, + exposurePointSupported, + focusMode, + focusPointSupported)); + } + + /// Releases the resources of the accessed camera. + /// + /// [cameraId] not used. + @override + Future dispose(int cameraId) async { + preview?.releaseFlutterSurfaceTexture(); + processCameraProvider?.unbindAll(); + } + + /// The camera has been initialized. + @override + Stream onCameraInitialized(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + /// The camera experienced an error. + @override + Stream onCameraError(int cameraId) { + return SystemServices.cameraErrorStreamController.stream + .map((String errorDescription) { + return CameraErrorEvent(cameraId, errorDescription); + }); + } + + /// The ui orientation changed. + @override + Stream onDeviceOrientationChanged() { + return SystemServices.deviceOrientationChangedStreamController.stream; + } + + /// Pause the active preview on the current frame for the selected camera. + /// + /// [cameraId] not used. + @override + Future pausePreview(int cameraId) async { + _unbindPreviewFromLifecycle(); + _previewIsPaused = true; + } + + /// Resume the paused preview for the selected camera. + /// + /// [cameraId] not used. + @override + Future resumePreview(int cameraId) async { + await _bindPreviewToLifecycle(); + _previewIsPaused = false; + } + + /// Returns a widget showing a live camera preview. + @override + Widget buildPreview(int cameraId) { + return FutureBuilder( + future: _bindPreviewToLifecycle(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.none: + case ConnectionState.waiting: + case ConnectionState.active: + // Do nothing while waiting for preview to be bound to lifecyle. + return const SizedBox.shrink(); + case ConnectionState.done: + return Texture(textureId: cameraId); + } + }); + } + + // Methods for binding UseCases to the lifecycle of the camera controlled + // by a ProcessCameraProvider instance: + + /// Binds [preview] instance to the camera lifecycle controlled by the + /// [processCameraProvider]. + Future _bindPreviewToLifecycle() async { + assert(processCameraProvider != null); + assert(cameraSelector != null); + + if (previewIsBound || _previewIsPaused) { + // Only bind if preview is not already bound or intentionally paused. + return; + } + + camera = await processCameraProvider! + .bindToLifecycle(cameraSelector!, [preview!]); + previewIsBound = true; + } + + /// Unbinds [preview] instance to camera lifecycle controlled by the + /// [processCameraProvider]. + void _unbindPreviewFromLifecycle() { + if (preview == null || !previewIsBound) { + return; + } + + assert(processCameraProvider != null); + + processCameraProvider!.unbind([preview!]); + previewIsBound = false; + } + + // Methods for mapping Flutter camera constants to CameraX constants: + + /// Returns [CameraSelector] lens direction that maps to specified + /// [CameraLensDirection]. + int _getCameraSelectorLensDirection(CameraLensDirection lensDirection) { + switch (lensDirection) { + case CameraLensDirection.front: + return CameraSelector.lensFacingFront; + case CameraLensDirection.back: + return CameraSelector.lensFacingBack; + case CameraLensDirection.external: + return CameraSelector.lensFacingExternal; + } + } + + /// Returns [Surface] target rotation constant that maps to specified sensor + /// orientation. + int _getTargetRotation(int sensorOrientation) { + switch (sensorOrientation) { + case 90: + return Surface.ROTATION_90; + case 180: + return Surface.ROTATION_180; + case 270: + return Surface.ROTATION_270; + case 0: + return Surface.ROTATION_0; + default: + throw ArgumentError( + '"$sensorOrientation" is not a valid sensor orientation value'); + } + } + + /// Returns [ResolutionInfo] that maps to the specified resolution preset for + /// a camera preview. + ResolutionInfo? _getTargetResolutionForPreview(ResolutionPreset? resolution) { + // TODO(camsim99): Implement resolution configuration. + // https://github.com/flutter/flutter/issues/120462 + return null; + } + + // Methods for calls that need to be tested: + + /// Requests camera permissions. + @visibleForTesting + Future requestCameraPermissions(bool enableAudio) async { + await SystemServices.requestCameraPermissions(enableAudio); + } + + /// Subscribes the plugin as a listener to changes in device orientation. + @visibleForTesting + void startListeningForDeviceOrientationChange( + bool cameraIsFrontFacing, int sensorOrientation) { + SystemServices.startListeningForDeviceOrientationChange( + cameraIsFrontFacing, sensorOrientation); + } + + /// Returns a [CameraSelector] based on the specified camera lens direction. + @visibleForTesting + CameraSelector createCameraSelector(int cameraSelectorLensDirection) { + switch (cameraSelectorLensDirection) { + case CameraSelector.lensFacingFront: + return CameraSelector.getDefaultFrontCamera(); + case CameraSelector.lensFacingBack: + return CameraSelector.getDefaultBackCamera(); + default: + return CameraSelector(lensFacing: cameraSelectorLensDirection); + } + } + + /// Returns a [Preview] configured with the specified target rotation and + /// resolution. + @visibleForTesting + Preview createPreview(int targetRotation, ResolutionInfo? targetResolution) { + return Preview( + targetRotation: targetRotation, targetResolution: targetResolution); } } diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart index 9c6564a06c08..0a1b3ce3b285 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart @@ -2,20 +2,24 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'camera.dart'; import 'camera_info.dart'; import 'camera_selector.dart'; -import 'camerax_library.pigeon.dart'; +import 'camerax_library.g.dart'; import 'java_object.dart'; import 'process_camera_provider.dart'; +import 'system_services.dart'; /// Handles initialization of Flutter APIs for the Android CameraX library. class AndroidCameraXCameraFlutterApis { /// Creates a [AndroidCameraXCameraFlutterApis]. AndroidCameraXCameraFlutterApis({ JavaObjectFlutterApiImpl? javaObjectFlutterApi, + CameraFlutterApiImpl? cameraFlutterApi, CameraInfoFlutterApiImpl? cameraInfoFlutterApi, CameraSelectorFlutterApiImpl? cameraSelectorFlutterApi, ProcessCameraProviderFlutterApiImpl? processCameraProviderFlutterApi, + SystemServicesFlutterApiImpl? systemServicesFlutterApi, }) { this.javaObjectFlutterApi = javaObjectFlutterApi ?? JavaObjectFlutterApiImpl(); @@ -25,6 +29,9 @@ class AndroidCameraXCameraFlutterApis { cameraSelectorFlutterApi ?? CameraSelectorFlutterApiImpl(); this.processCameraProviderFlutterApi = processCameraProviderFlutterApi ?? ProcessCameraProviderFlutterApiImpl(); + this.cameraFlutterApi = cameraFlutterApi ?? CameraFlutterApiImpl(); + this.systemServicesFlutterApi = + systemServicesFlutterApi ?? SystemServicesFlutterApiImpl(); } static bool _haveBeenSetUp = false; @@ -48,6 +55,12 @@ class AndroidCameraXCameraFlutterApis { late final ProcessCameraProviderFlutterApiImpl processCameraProviderFlutterApi; + /// Flutter Api for [Camera]. + late final CameraFlutterApiImpl cameraFlutterApi; + + /// Flutter Api for [SystemServices]. + late final SystemServicesFlutterApiImpl systemServicesFlutterApi; + /// Ensures all the Flutter APIs have been setup to receive calls from native code. void ensureSetUp() { if (!_haveBeenSetUp) { @@ -55,6 +68,8 @@ class AndroidCameraXCameraFlutterApis { CameraInfoFlutterApi.setup(cameraInfoFlutterApi); CameraSelectorFlutterApi.setup(cameraSelectorFlutterApi); ProcessCameraProviderFlutterApi.setup(processCameraProviderFlutterApi); + CameraFlutterApi.setup(cameraFlutterApi); + SystemServicesFlutterApi.setup(systemServicesFlutterApi); _haveBeenSetUp = true; } } diff --git a/packages/camera/camera_android_camerax/lib/src/camera.dart b/packages/camera/camera_android_camerax/lib/src/camera.dart new file mode 100644 index 000000000000..24ff30540b28 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/camera.dart @@ -0,0 +1,53 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart' show BinaryMessenger; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// The interface used to control the flow of data of use cases, control the +/// camera, and publich the state of the camera. +/// +/// See https://developer.android.com/reference/androidx/camera/core/Camera. +class Camera extends JavaObject { + /// Constructs a [Camera] that is not automatically attached to a native object. + Camera.detached({super.binaryMessenger, super.instanceManager}) + : super.detached() { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } +} + +/// Flutter API implementation of [Camera]. +class CameraFlutterApiImpl implements CameraFlutterApi { + /// Constructs a [CameraSelectorFlutterApiImpl]. + CameraFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + final InstanceManager instanceManager; + + @override + void create(int identifier) { + instanceManager.addHostCreatedInstance( + Camera.detached( + binaryMessenger: binaryMessenger, instanceManager: instanceManager), + identifier, + onCopy: (Camera original) { + return Camera.detached( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + }, + ); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/camera_info.dart b/packages/camera/camera_android_camerax/lib/src/camera_info.dart index d03426f40027..8c2c7bcf0aec 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_info.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_info.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart' show BinaryMessenger; import 'android_camera_camerax_flutter_api_impls.dart'; -import 'camerax_library.pigeon.dart'; +import 'camerax_library.g.dart'; import 'instance_manager.dart'; import 'java_object.dart'; diff --git a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart index 094147f208fe..f1d3c5fdb663 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_selector.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_selector.dart @@ -6,7 +6,7 @@ import 'package:flutter/services.dart'; import 'android_camera_camerax_flutter_api_impls.dart'; import 'camera_info.dart'; -import 'camerax_library.pigeon.dart'; +import 'camerax_library.g.dart'; import 'instance_manager.dart'; import 'java_object.dart'; @@ -44,10 +44,24 @@ class CameraSelector extends JavaObject { late final CameraSelectorHostApiImpl _api; /// ID for front facing lens. - static const int LENS_FACING_FRONT = 0; + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_FRONT(). + static const int lensFacingFront = 0; /// ID for back facing lens. - static const int LENS_FACING_BACK = 1; + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_BACK(). + static const int lensFacingBack = 1; + + /// ID for external lens. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_EXTERNAL(). + static const int lensFacingExternal = 2; + + /// ID for unknown lens. + /// + /// See https://developer.android.com/reference/androidx/camera/core/CameraSelector#LENS_FACING_UNKNOWN(). + static const int lensFacingUnknown = -1; /// Selector for default front facing camera. static CameraSelector getDefaultFrontCamera({ @@ -57,7 +71,7 @@ class CameraSelector extends JavaObject { return CameraSelector( binaryMessenger: binaryMessenger, instanceManager: instanceManager, - lensFacing: LENS_FACING_FRONT, + lensFacing: lensFacingFront, ); } @@ -69,7 +83,7 @@ class CameraSelector extends JavaObject { return CameraSelector( binaryMessenger: binaryMessenger, instanceManager: instanceManager, - lensFacing: LENS_FACING_BACK, + lensFacing: lensFacingBack, ); } diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart new file mode 100644 index 000000000000..1d315e5a1600 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart @@ -0,0 +1,855 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; + +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; + +class ResolutionInfo { + ResolutionInfo({ + required this.width, + required this.height, + }); + + int width; + int height; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['width'] = width; + pigeonMap['height'] = height; + return pigeonMap; + } + + static ResolutionInfo decode(Object message) { + final Map pigeonMap = message as Map; + return ResolutionInfo( + width: pigeonMap['width']! as int, + height: pigeonMap['height']! as int, + ); + } +} + +class CameraPermissionsErrorData { + CameraPermissionsErrorData({ + required this.errorCode, + required this.description, + }); + + String errorCode; + String description; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['errorCode'] = errorCode; + pigeonMap['description'] = description; + return pigeonMap; + } + + static CameraPermissionsErrorData decode(Object message) { + final Map pigeonMap = message as Map; + return CameraPermissionsErrorData( + errorCode: pigeonMap['errorCode']! as String, + description: pigeonMap['description']! as String, + ); + } +} + +class _JavaObjectHostApiCodec extends StandardMessageCodec { + const _JavaObjectHostApiCodec(); +} + +class JavaObjectHostApi { + /// Constructor for [JavaObjectHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + JavaObjectHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _JavaObjectHostApiCodec(); + + Future dispose(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaObjectHostApi.dispose', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _JavaObjectFlutterApiCodec extends StandardMessageCodec { + const _JavaObjectFlutterApiCodec(); +} + +abstract class JavaObjectFlutterApi { + static const MessageCodec codec = _JavaObjectFlutterApiCodec(); + + void dispose(int identifier); + static void setup(JavaObjectFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaObjectFlutterApi.dispose', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.JavaObjectFlutterApi.dispose was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.JavaObjectFlutterApi.dispose was null, expected non-null int.'); + api.dispose(arg_identifier!); + return; + }); + } + } + } +} + +class _CameraInfoHostApiCodec extends StandardMessageCodec { + const _CameraInfoHostApiCodec(); +} + +class CameraInfoHostApi { + /// Constructor for [CameraInfoHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + CameraInfoHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _CameraInfoHostApiCodec(); + + Future getSensorRotationDegrees(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as int?)!; + } + } +} + +class _CameraInfoFlutterApiCodec extends StandardMessageCodec { + const _CameraInfoFlutterApiCodec(); +} + +abstract class CameraInfoFlutterApi { + static const MessageCodec codec = _CameraInfoFlutterApiCodec(); + + void create(int identifier); + static void setup(CameraInfoFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraInfoFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraInfoFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.CameraInfoFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +class _CameraSelectorHostApiCodec extends StandardMessageCodec { + const _CameraSelectorHostApiCodec(); +} + +class CameraSelectorHostApi { + /// Constructor for [CameraSelectorHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + CameraSelectorHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _CameraSelectorHostApiCodec(); + + Future create(int arg_identifier, int? arg_lensFacing) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraSelectorHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_lensFacing]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future> filter( + int arg_identifier, List arg_cameraInfoIds) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraSelectorHostApi.filter', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_cameraInfoIds]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as List?)!.cast(); + } + } +} + +class _CameraSelectorFlutterApiCodec extends StandardMessageCodec { + const _CameraSelectorFlutterApiCodec(); +} + +abstract class CameraSelectorFlutterApi { + static const MessageCodec codec = _CameraSelectorFlutterApiCodec(); + + void create(int identifier, int? lensFacing); + static void setup(CameraSelectorFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraSelectorFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraSelectorFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.CameraSelectorFlutterApi.create was null, expected non-null int.'); + final int? arg_lensFacing = (args[1] as int?); + api.create(arg_identifier!, arg_lensFacing); + return; + }); + } + } + } +} + +class _ProcessCameraProviderHostApiCodec extends StandardMessageCodec { + const _ProcessCameraProviderHostApiCodec(); +} + +class ProcessCameraProviderHostApi { + /// Constructor for [ProcessCameraProviderHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + ProcessCameraProviderHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _ProcessCameraProviderHostApiCodec(); + + Future getInstance() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as int?)!; + } + } + + Future> getAvailableCameraInfos(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as List?)!.cast(); + } + } + + Future bindToLifecycle(int arg_identifier, + int arg_cameraSelectorIdentifier, List arg_useCaseIds) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel.send([ + arg_identifier, + arg_cameraSelectorIdentifier, + arg_useCaseIds + ]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as int?)!; + } + } + + Future unbind(int arg_identifier, List arg_useCaseIds) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier, arg_useCaseIds]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future unbindAll(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.unbindAll', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _ProcessCameraProviderFlutterApiCodec extends StandardMessageCodec { + const _ProcessCameraProviderFlutterApiCodec(); +} + +abstract class ProcessCameraProviderFlutterApi { + static const MessageCodec codec = + _ProcessCameraProviderFlutterApiCodec(); + + void create(int identifier); + static void setup(ProcessCameraProviderFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +class _CameraFlutterApiCodec extends StandardMessageCodec { + const _CameraFlutterApiCodec(); +} + +abstract class CameraFlutterApi { + static const MessageCodec codec = _CameraFlutterApiCodec(); + + void create(int identifier); + static void setup(CameraFlutterApi? api, {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.CameraFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} + +class _SystemServicesHostApiCodec extends StandardMessageCodec { + const _SystemServicesHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CameraPermissionsErrorData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CameraPermissionsErrorData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class SystemServicesHostApi { + /// Constructor for [SystemServicesHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + SystemServicesHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _SystemServicesHostApiCodec(); + + Future requestCameraPermissions( + bool arg_enableAudio) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.requestCameraPermissions', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_enableAudio]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return (replyMap['result'] as CameraPermissionsErrorData?); + } + } + + Future startListeningForDeviceOrientationChange( + bool arg_isFrontFacing, int arg_sensorOrientation) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_isFrontFacing, arg_sensorOrientation]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future stopListeningForDeviceOrientationChange() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.stopListeningForDeviceOrientationChange', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } +} + +class _SystemServicesFlutterApiCodec extends StandardMessageCodec { + const _SystemServicesFlutterApiCodec(); +} + +abstract class SystemServicesFlutterApi { + static const MessageCodec codec = _SystemServicesFlutterApiCodec(); + + void onDeviceOrientationChanged(String orientation); + void onCameraError(String errorDescription); + static void setup(SystemServicesFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesFlutterApi.onDeviceOrientationChanged', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onDeviceOrientationChanged was null.'); + final List args = (message as List?)!; + final String? arg_orientation = (args[0] as String?); + assert(arg_orientation != null, + 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onDeviceOrientationChanged was null, expected non-null String.'); + api.onDeviceOrientationChanged(arg_orientation!); + return; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError was null.'); + final List args = (message as List?)!; + final String? arg_errorDescription = (args[0] as String?); + assert(arg_errorDescription != null, + 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError was null, expected non-null String.'); + api.onCameraError(arg_errorDescription!); + return; + }); + } + } + } +} + +class _PreviewHostApiCodec extends StandardMessageCodec { + const _PreviewHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is ResolutionInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is ResolutionInfo) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return ResolutionInfo.decode(readValue(buffer)!); + + case 129: + return ResolutionInfo.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +class PreviewHostApi { + /// Constructor for [PreviewHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + PreviewHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _PreviewHostApiCodec(); + + Future create(int arg_identifier, int? arg_rotation, + ResolutionInfo? arg_targetResolution) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.create', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = await channel + .send([arg_identifier, arg_rotation, arg_targetResolution]) + as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future setSurfaceProvider(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as int?)!; + } + } + + Future releaseFlutterSurfaceTexture() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.releaseFlutterSurfaceTexture', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else { + return; + } + } + + Future getResolutionInfo(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.getResolutionInfo', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as ResolutionInfo?)!; + } + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.pigeon.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.pigeon.dart deleted file mode 100644 index c0b052378def..000000000000 --- a/packages/camera/camera_android_camerax/lib/src/camerax_library.pigeon.dart +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// Autogenerated from Pigeon (v3.2.9), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import -import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; - -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; -import 'package:flutter/services.dart'; - -class _JavaObjectHostApiCodec extends StandardMessageCodec { - const _JavaObjectHostApiCodec(); -} - -class JavaObjectHostApi { - /// Constructor for [JavaObjectHostApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - JavaObjectHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = _JavaObjectHostApiCodec(); - - Future dispose(int arg_identifier) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaObjectHostApi.dispose', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } -} - -class _JavaObjectFlutterApiCodec extends StandardMessageCodec { - const _JavaObjectFlutterApiCodec(); -} - -abstract class JavaObjectFlutterApi { - static const MessageCodec codec = _JavaObjectFlutterApiCodec(); - - void dispose(int identifier); - static void setup(JavaObjectFlutterApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaObjectFlutterApi.dispose', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.JavaObjectFlutterApi.dispose was null.'); - final List args = (message as List?)!; - final int? arg_identifier = (args[0] as int?); - assert(arg_identifier != null, - 'Argument for dev.flutter.pigeon.JavaObjectFlutterApi.dispose was null, expected non-null int.'); - api.dispose(arg_identifier!); - return; - }); - } - } - } -} - -class _CameraInfoHostApiCodec extends StandardMessageCodec { - const _CameraInfoHostApiCodec(); -} - -class CameraInfoHostApi { - /// Constructor for [CameraInfoHostApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - CameraInfoHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = _CameraInfoHostApiCodec(); - - Future getSensorRotationDegrees(int arg_identifier) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else if (replyMap['result'] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyMap['result'] as int?)!; - } - } -} - -class _CameraInfoFlutterApiCodec extends StandardMessageCodec { - const _CameraInfoFlutterApiCodec(); -} - -abstract class CameraInfoFlutterApi { - static const MessageCodec codec = _CameraInfoFlutterApiCodec(); - - void create(int identifier); - static void setup(CameraInfoFlutterApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraInfoFlutterApi.create', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.CameraInfoFlutterApi.create was null.'); - final List args = (message as List?)!; - final int? arg_identifier = (args[0] as int?); - assert(arg_identifier != null, - 'Argument for dev.flutter.pigeon.CameraInfoFlutterApi.create was null, expected non-null int.'); - api.create(arg_identifier!); - return; - }); - } - } - } -} - -class _CameraSelectorHostApiCodec extends StandardMessageCodec { - const _CameraSelectorHostApiCodec(); -} - -class CameraSelectorHostApi { - /// Constructor for [CameraSelectorHostApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - CameraSelectorHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = _CameraSelectorHostApiCodec(); - - Future create(int arg_identifier, int? arg_lensFacing) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraSelectorHostApi.create', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier, arg_lensFacing]) - as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else { - return; - } - } - - Future> filter( - int arg_identifier, List arg_cameraInfoIds) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraSelectorHostApi.filter', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier, arg_cameraInfoIds]) - as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else if (replyMap['result'] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyMap['result'] as List?)!.cast(); - } - } -} - -class _CameraSelectorFlutterApiCodec extends StandardMessageCodec { - const _CameraSelectorFlutterApiCodec(); -} - -abstract class CameraSelectorFlutterApi { - static const MessageCodec codec = _CameraSelectorFlutterApiCodec(); - - void create(int identifier, int? lensFacing); - static void setup(CameraSelectorFlutterApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraSelectorFlutterApi.create', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.CameraSelectorFlutterApi.create was null.'); - final List args = (message as List?)!; - final int? arg_identifier = (args[0] as int?); - assert(arg_identifier != null, - 'Argument for dev.flutter.pigeon.CameraSelectorFlutterApi.create was null, expected non-null int.'); - final int? arg_lensFacing = (args[1] as int?); - api.create(arg_identifier!, arg_lensFacing); - return; - }); - } - } - } -} - -class _ProcessCameraProviderHostApiCodec extends StandardMessageCodec { - const _ProcessCameraProviderHostApiCodec(); -} - -class ProcessCameraProviderHostApi { - /// Constructor for [ProcessCameraProviderHostApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - ProcessCameraProviderHostApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = - _ProcessCameraProviderHostApiCodec(); - - Future getInstance() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance', codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else if (replyMap['result'] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyMap['result'] as int?)!; - } - } - - Future> getAvailableCameraInfos(int arg_identifier) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos', - codec, - binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; - throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], - ); - } else if (replyMap['result'] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyMap['result'] as List?)!.cast(); - } - } -} - -class _ProcessCameraProviderFlutterApiCodec extends StandardMessageCodec { - const _ProcessCameraProviderFlutterApiCodec(); -} - -abstract class ProcessCameraProviderFlutterApi { - static const MessageCodec codec = - _ProcessCameraProviderFlutterApiCodec(); - - void create(int identifier); - static void setup(ProcessCameraProviderFlutterApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create was null.'); - final List args = (message as List?)!; - final int? arg_identifier = (args[0] as int?); - assert(arg_identifier != null, - 'Argument for dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create was null, expected non-null int.'); - api.create(arg_identifier!); - return; - }); - } - } - } -} diff --git a/packages/camera/camera_android_camerax/lib/src/java_object.dart b/packages/camera/camera_android_camerax/lib/src/java_object.dart index 36a29ed0517b..f6127d4a8106 100644 --- a/packages/camera/camera_android_camerax/lib/src/java_object.dart +++ b/packages/camera/camera_android_camerax/lib/src/java_object.dart @@ -5,7 +5,7 @@ import 'package:flutter/foundation.dart' show immutable; import 'package:flutter/services.dart'; -import 'camerax_library.pigeon.dart'; +import 'camerax_library.g.dart'; import 'instance_manager.dart'; /// Root of the Java class hierarchy. diff --git a/packages/camera/camera_android_camerax/lib/src/preview.dart b/packages/camera/camera_android_camerax/lib/src/preview.dart new file mode 100644 index 000000000000..602bcb3da76a --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/preview.dart @@ -0,0 +1,126 @@ +// 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/services.dart' show BinaryMessenger; + +import 'camerax_library.g.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; +import 'use_case.dart'; + +/// Use case that provides a camera preview stream for display. +/// +/// See https://developer.android.com/reference/androidx/camera/core/Preview. +class Preview extends UseCase { + /// Creates a [Preview]. + Preview( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + this.targetRotation, + this.targetResolution}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = PreviewHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + _api.createFromInstance(this, targetRotation, targetResolution); + } + + /// Constructs a [Preview] that is not automatically attached to a native object. + Preview.detached( + {BinaryMessenger? binaryMessenger, + InstanceManager? instanceManager, + this.targetRotation, + this.targetResolution}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = PreviewHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + } + + late final PreviewHostApiImpl _api; + + /// Target rotation of the camera used for the preview stream. + final int? targetRotation; + + /// Target resolution of the camera preview stream. + final ResolutionInfo? targetResolution; + + /// Sets the surface provider for the preview stream. + /// + /// Returns the ID of the FlutterSurfaceTextureEntry used on the native end + /// used to display the preview stream on a [Texture] of the same ID. + Future setSurfaceProvider() { + return _api.setSurfaceProviderFromInstance(this); + } + + /// Releases Flutter surface texture used to provide a surface for the preview + /// stream. + void releaseFlutterSurfaceTexture() { + _api.releaseFlutterSurfaceTextureFromInstance(); + } + + /// Retrieves the selected resolution information of this [Preview]. + Future getResolutionInfo() { + return _api.getResolutionInfoFromInstance(this); + } +} + +/// Host API implementation of [Preview]. +class PreviewHostApiImpl extends PreviewHostApi { + /// Constructs a [PreviewHostApiImpl]. + PreviewHostApiImpl({this.binaryMessenger, InstanceManager? instanceManager}) { + this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + } + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Maintains instances stored to communicate with native language objects. + late final InstanceManager instanceManager; + + /// Creates a [Preview] with the target rotation provided if specified. + void createFromInstance( + Preview instance, int? targetRotation, ResolutionInfo? targetResolution) { + final int identifier = instanceManager.addDartCreatedInstance(instance, + onCopy: (Preview original) { + return Preview.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager, + targetRotation: original.targetRotation); + }); + create(identifier, targetRotation, targetResolution); + } + + /// Sets the surface provider of the specified [Preview] instance and returns + /// the ID corresponding to the surface it will provide. + Future setSurfaceProviderFromInstance(Preview instance) async { + final int? identifier = instanceManager.getIdentifier(instance); + assert(identifier != null, + 'No Preview has the identifer of that requested to set the surface provider on.'); + + final int surfaceTextureEntryId = await setSurfaceProvider(identifier!); + return surfaceTextureEntryId; + } + + /// Releases Flutter surface texture used to provide a surface for the preview + /// stream if a surface provider was set for a [Preview] instance. + void releaseFlutterSurfaceTextureFromInstance() { + releaseFlutterSurfaceTexture(); + } + + /// Gets the resolution information of the specified [Preview] instance. + Future getResolutionInfoFromInstance(Preview instance) async { + final int? identifier = instanceManager.getIdentifier(instance); + assert(identifier != null, + 'No Preview has the identifer of that requested to get the resolution information for.'); + + final ResolutionInfo resolutionInfo = await getResolutionInfo(identifier!); + return resolutionInfo; + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart b/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart index 5a67fa7e4dc3..ed9e820a1fa0 100644 --- a/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart +++ b/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart @@ -5,10 +5,13 @@ import 'package:flutter/services.dart'; import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camera.dart'; import 'camera_info.dart'; -import 'camerax_library.pigeon.dart'; +import 'camera_selector.dart'; +import 'camerax_library.g.dart'; import 'instance_manager.dart'; import 'java_object.dart'; +import 'use_case.dart'; /// Provides an object to manage the camera. /// @@ -42,6 +45,25 @@ class ProcessCameraProvider extends JavaObject { Future> getAvailableCameraInfos() { return _api.getAvailableCameraInfosFromInstances(this); } + + /// Binds the specified [UseCase]s to the lifecycle of the camera that it + /// returns. + Future bindToLifecycle( + CameraSelector cameraSelector, List useCases) { + return _api.bindToLifecycleFromInstances(this, cameraSelector, useCases); + } + + /// Unbinds specified [UseCase]s from the lifecycle of the camera that this + /// instance tracks. + void unbind(List useCases) { + _api.unbindFromInstances(this, useCases); + } + + /// Unbinds all previously bound [UseCase]s from the lifecycle of the camera + /// that this tracks. + void unbindAll() { + _api.unbindAllFromInstances(this); + } } /// Host API implementation of [ProcessCameraProvider]. @@ -69,22 +91,71 @@ class ProcessCameraProviderHostApiImpl extends ProcessCameraProviderHostApi { as ProcessCameraProvider; } + /// Gets identifier that the [instanceManager] has set for + /// the [ProcessCameraProvider] instance. + int getProcessCameraProviderIdentifier(ProcessCameraProvider instance) { + final int? identifier = instanceManager.getIdentifier(instance); + + assert(identifier != null, + 'No ProcessCameraProvider has the identifer of that which was requested.'); + return identifier!; + } + /// Retrives the list of CameraInfos corresponding to the available cameras. Future> getAvailableCameraInfosFromInstances( ProcessCameraProvider instance) async { - int? identifier = instanceManager.getIdentifier(instance); - identifier ??= instanceManager.addDartCreatedInstance(instance, - onCopy: (ProcessCameraProvider original) { - return ProcessCameraProvider.detached( - binaryMessenger: binaryMessenger, instanceManager: instanceManager); - }); - + final int identifier = getProcessCameraProviderIdentifier(instance); final List cameraInfos = await getAvailableCameraInfos(identifier); return cameraInfos .map((int? id) => instanceManager.getInstanceWithWeakReference(id!)! as CameraInfo) .toList(); } + + /// Binds the specified [UseCase]s to the lifecycle of the camera which + /// the provided [ProcessCameraProvider] instance tracks. + /// + /// The instance of the camera whose lifecycle the [UseCase]s are bound to + /// is returned. + Future bindToLifecycleFromInstances( + ProcessCameraProvider instance, + CameraSelector cameraSelector, + List useCases, + ) async { + final int identifier = getProcessCameraProviderIdentifier(instance); + final List useCaseIds = useCases + .map((UseCase useCase) => instanceManager.getIdentifier(useCase)!) + .toList(); + + final int cameraIdentifier = await bindToLifecycle( + identifier, + instanceManager.getIdentifier(cameraSelector)!, + useCaseIds, + ); + return instanceManager.getInstanceWithWeakReference(cameraIdentifier)! + as Camera; + } + + /// Unbinds specified [UseCase]s from the lifecycle of the camera which the + /// provided [ProcessCameraProvider] instance tracks. + void unbindFromInstances( + ProcessCameraProvider instance, + List useCases, + ) { + final int identifier = getProcessCameraProviderIdentifier(instance); + final List useCaseIds = useCases + .map((UseCase useCase) => instanceManager.getIdentifier(useCase)!) + .toList(); + + unbind(identifier, useCaseIds); + } + + /// Unbinds all previously bound [UseCase]s from the lifecycle of the camera + /// which the provided [ProcessCameraProvider] instance tracks. + void unbindAllFromInstances(ProcessCameraProvider instance) { + final int identifier = getProcessCameraProviderIdentifier(instance); + unbindAll(identifier); + } } /// Flutter API Implementation of [ProcessCameraProvider]. diff --git a/packages/camera/camera_android_camerax/lib/src/surface.dart b/packages/camera/camera_android_camerax/lib/src/surface.dart new file mode 100644 index 000000000000..ea8cf8cb751e --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/surface.dart @@ -0,0 +1,34 @@ +// 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 'java_object.dart'; + +/// Handle onto the raw buffer managed by screen compositor. +/// +/// See https://developer.android.com/reference/android/view/Surface.html. +class Surface extends JavaObject { + /// Creates a detached [UseCase]. + Surface.detached({super.binaryMessenger, super.instanceManager}) + : super.detached(); + + /// Rotation constant to signify the natural orientation. + /// + /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_0. + static const int ROTATION_0 = 0; + + /// Rotation constant to signify a 90 degrees rotation. + /// + /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_90. + static const int ROTATION_90 = 1; + + /// Rotation constant to signify a 180 degrees rotation. + /// + /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_180. + static const int ROTATION_180 = 2; + + /// Rotation constant to signify a 270 degrees rotation. + /// + /// See https://developer.android.com/reference/android/view/Surface.html#ROTATION_270. + static const int ROTATION_270 = 3; +} diff --git a/packages/camera/camera_android_camerax/lib/src/system_services.dart b/packages/camera/camera_android_camerax/lib/src/system_services.dart new file mode 100644 index 000000000000..e108b6140bed --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/system_services.dart @@ -0,0 +1,147 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:camera_platform_interface/camera_platform_interface.dart' + show CameraException, DeviceOrientationChangedEvent; +import 'package:flutter/services.dart'; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; + +// Ignoring lint indicating this class only contains static members +// as this class is a wrapper for various Android system services. +// ignore_for_file: avoid_classes_with_only_static_members + +/// Utility class that offers access to Android system services needed for +/// camera usage and other informational streams. +class SystemServices { + /// Stream that emits the device orientation whenever it is changed. + /// + /// Values may start being added to the stream once + /// `startListeningForDeviceOrientationChange(...)` is called. + static final StreamController + deviceOrientationChangedStreamController = + StreamController.broadcast(); + + /// Stream that emits the errors caused by camera usage on the native side. + static final StreamController cameraErrorStreamController = + StreamController.broadcast(); + + /// Requests permission to access the camera and audio if specified. + static Future requestCameraPermissions(bool enableAudio, + {BinaryMessenger? binaryMessenger}) { + final SystemServicesHostApiImpl api = + SystemServicesHostApiImpl(binaryMessenger: binaryMessenger); + + return api.sendCameraPermissionsRequest(enableAudio); + } + + /// Requests that [deviceOrientationChangedStreamController] start + /// emitting values for any change in device orientation. + static void startListeningForDeviceOrientationChange( + bool isFrontFacing, int sensorOrientation, + {BinaryMessenger? binaryMessenger}) { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + final SystemServicesHostApi api = + SystemServicesHostApi(binaryMessenger: binaryMessenger); + + api.startListeningForDeviceOrientationChange( + isFrontFacing, sensorOrientation); + } + + /// Stops the [deviceOrientationChangedStreamController] from emitting values + /// for changes in device orientation. + static void stopListeningForDeviceOrientationChange( + {BinaryMessenger? binaryMessenger}) { + final SystemServicesHostApi api = + SystemServicesHostApi(binaryMessenger: binaryMessenger); + + api.stopListeningForDeviceOrientationChange(); + } +} + +/// Host API implementation of [SystemServices]. +class SystemServicesHostApiImpl extends SystemServicesHostApi { + /// Creates a [SystemServicesHostApiImpl]. + SystemServicesHostApiImpl({this.binaryMessenger}) + : super(binaryMessenger: binaryMessenger); + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Requests permission to access the camera and audio if specified. + /// + /// Will complete normally if permissions are successfully granted; otherwise, + /// will throw a [CameraException]. + Future sendCameraPermissionsRequest(bool enableAudio) async { + final CameraPermissionsErrorData? error = + await requestCameraPermissions(enableAudio); + + if (error != null) { + throw CameraException( + error.errorCode, + error.description, + ); + } + } +} + +/// Flutter API implementation of [SystemServices]. +class SystemServicesFlutterApiImpl implements SystemServicesFlutterApi { + /// Constructs a [SystemServicesFlutterApiImpl]. + SystemServicesFlutterApiImpl({ + this.binaryMessenger, + }); + + /// Receives binary data across the Flutter platform barrier. + /// + /// If it is null, the default BinaryMessenger will be used which routes to + /// the host platform. + final BinaryMessenger? binaryMessenger; + + /// Callback method for any changes in device orientation. + /// + /// Will only be called if + /// `SystemServices.startListeningForDeviceOrientationChange(...)` was called + /// to start listening for device orientation updates. + @override + void onDeviceOrientationChanged(String orientation) { + final DeviceOrientation deviceOrientation = + deserializeDeviceOrientation(orientation); + if (deviceOrientation == null) { + return; + } + SystemServices.deviceOrientationChangedStreamController + .add(DeviceOrientationChangedEvent(deviceOrientation)); + } + + /// Deserializes device orientation in [String] format into a + /// [DeviceOrientation]. + DeviceOrientation deserializeDeviceOrientation(String orientation) { + switch (orientation) { + case 'LANDSCAPE_LEFT': + return DeviceOrientation.landscapeLeft; + case 'LANDSCAPE_RIGHT': + return DeviceOrientation.landscapeRight; + case 'PORTRAIT_DOWN': + return DeviceOrientation.portraitDown; + case 'PORTRAIT_UP': + return DeviceOrientation.portraitUp; + default: + throw ArgumentError( + '"$orientation" is not a valid DeviceOrientation value'); + } + } + + /// Callback method for any errors caused by camera usage on the Java side. + @override + void onCameraError(String errorDescription) { + SystemServices.cameraErrorStreamController.add(errorDescription); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/use_case.dart b/packages/camera/camera_android_camerax/lib/src/use_case.dart new file mode 100644 index 000000000000..f8910d9c5347 --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/use_case.dart @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'java_object.dart'; + +/// An object representing the different functionalitites of the camera. +/// +/// See https://developer.android.com/reference/androidx/camera/core/UseCase. +class UseCase extends JavaObject { + /// Creates a detached [UseCase]. + UseCase.detached({super.binaryMessenger, super.instanceManager}) + : super.detached(); +} diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index 4d7d96910246..4172cd7db073 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -6,8 +6,8 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( - dartOut: 'lib/src/camerax_library.pigeon.dart', - dartTestOut: 'test/test_camerax_library.pigeon.dart', + dartOut: 'lib/src/camerax_library.g.dart', + dartTestOut: 'test/test_camerax_library.g.dart', dartOptions: DartOptions(copyrightHeader: [ 'Copyright 2013 The Flutter Authors. All rights reserved.', 'Use of this source code is governed by a BSD-style license that can be', @@ -26,6 +26,26 @@ import 'package:pigeon/pigeon.dart'; ), ), ) +class ResolutionInfo { + ResolutionInfo({ + required this.width, + required this.height, + }); + + int width; + int height; +} + +class CameraPermissionsErrorData { + CameraPermissionsErrorData({ + required this.errorCode, + required this.description, + }); + + String errorCode; + String description; +} + @HostApi(dartHostTestHandler: 'TestJavaObjectHostApi') abstract class JavaObjectHostApi { void dispose(int identifier); @@ -64,9 +84,50 @@ abstract class ProcessCameraProviderHostApi { int getInstance(); List getAvailableCameraInfos(int identifier); + + int bindToLifecycle( + int identifier, int cameraSelectorIdentifier, List useCaseIds); + + void unbind(int identifier, List useCaseIds); + + void unbindAll(int identifier); } @FlutterApi() abstract class ProcessCameraProviderFlutterApi { void create(int identifier); } + +@FlutterApi() +abstract class CameraFlutterApi { + void create(int identifier); +} + +@HostApi(dartHostTestHandler: 'TestSystemServicesHostApi') +abstract class SystemServicesHostApi { + @async + CameraPermissionsErrorData? requestCameraPermissions(bool enableAudio); + + void startListeningForDeviceOrientationChange( + bool isFrontFacing, int sensorOrientation); + + void stopListeningForDeviceOrientationChange(); +} + +@FlutterApi() +abstract class SystemServicesFlutterApi { + void onDeviceOrientationChanged(String orientation); + + void onCameraError(String errorDescription); +} + +@HostApi(dartHostTestHandler: 'TestPreviewHostApi') +abstract class PreviewHostApi { + void create(int identifier, int? rotation, ResolutionInfo? targetResolution); + + int setSurfaceProvider(int identifier); + + void releaseFlutterSurfaceTexture(); + + ResolutionInfo getResolutionInfo(int identifier); +} diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 9873db1a0121..f1496c640497 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -21,10 +21,14 @@ dependencies: camera_platform_interface: ^2.2.0 flutter: sdk: flutter + integration_test: + sdk: flutter + stream_transform: ^2.1.0 dev_dependencies: + async: ^2.5.0 build_runner: ^2.1.4 flutter_test: sdk: flutter - mockito: ^5.1.0 + mockito: ^5.3.2 pigeon: ^3.2.6 diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart new file mode 100644 index 000000000000..acfaf16b9ac4 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart @@ -0,0 +1,405 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:camera_android_camerax/camera_android_camerax.dart'; +import 'package:camera_android_camerax/src/camera.dart'; +import 'package:camera_android_camerax/src/camera_info.dart'; +import 'package:camera_android_camerax/src/camera_selector.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/preview.dart'; +import 'package:camera_android_camerax/src/process_camera_provider.dart'; +import 'package:camera_android_camerax/src/system_services.dart'; +import 'package:camera_android_camerax/src/use_case.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart' show DeviceOrientation; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'android_camera_camerax_test.mocks.dart'; + +@GenerateNiceMocks(>[ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +@GenerateMocks([BuildContext]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('Should fetch CameraDescription instances for available cameras', + () async { + // Arrange + final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + camera.processCameraProvider = MockProcessCameraProvider(); + final List returnData = [ + { + 'name': 'Camera 0', + 'lensFacing': 'back', + 'sensorOrientation': 0 + }, + { + 'name': 'Camera 1', + 'lensFacing': 'front', + 'sensorOrientation': 90 + } + ]; + + // Create mocks to use + final MockCameraInfo mockFrontCameraInfo = MockCameraInfo(); + final MockCameraInfo mockBackCameraInfo = MockCameraInfo(); + + // Mock calls to native platform + when(camera.processCameraProvider!.getAvailableCameraInfos()).thenAnswer( + (_) async => [mockBackCameraInfo, mockFrontCameraInfo]); + when(camera.mockBackCameraSelector + .filter([mockFrontCameraInfo])) + .thenAnswer((_) async => []); + when(camera.mockBackCameraSelector + .filter([mockBackCameraInfo])) + .thenAnswer((_) async => [mockBackCameraInfo]); + when(camera.mockFrontCameraSelector + .filter([mockBackCameraInfo])) + .thenAnswer((_) async => []); + when(camera.mockFrontCameraSelector + .filter([mockFrontCameraInfo])) + .thenAnswer((_) async => [mockFrontCameraInfo]); + when(mockBackCameraInfo.getSensorRotationDegrees()) + .thenAnswer((_) async => 0); + when(mockFrontCameraInfo.getSensorRotationDegrees()) + .thenAnswer((_) async => 90); + + final List cameraDescriptions = + await camera.availableCameras(); + + expect(cameraDescriptions.length, returnData.length); + for (int i = 0; i < returnData.length; i++) { + final Map typedData = + (returnData[i] as Map).cast(); + final CameraDescription cameraDescription = CameraDescription( + name: typedData['name']! as String, + lensDirection: (typedData['lensFacing']! as String) == 'front' + ? CameraLensDirection.front + : CameraLensDirection.back, + sensorOrientation: typedData['sensorOrientation']! as int, + ); + expect(cameraDescriptions[i], cameraDescription); + } + }); + + test( + 'createCamera requests permissions, starts listening for device orientation changes, and returns flutter surface texture ID', + () async { + final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + camera.processCameraProvider = MockProcessCameraProvider(); + const CameraLensDirection testLensDirection = CameraLensDirection.back; + const int testSensorOrientation = 90; + const CameraDescription testCameraDescription = CameraDescription( + name: 'cameraName', + lensDirection: testLensDirection, + sensorOrientation: testSensorOrientation); + const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; + const bool enableAudio = true; + const int testSurfaceTextureId = 6; + + when(camera.testPreview.setSurfaceProvider()) + .thenAnswer((_) async => testSurfaceTextureId); + + expect( + await camera.createCamera(testCameraDescription, testResolutionPreset, + enableAudio: enableAudio), + equals(testSurfaceTextureId)); + + // Verify permissions are requested and the camera starts listening for device orientation changes. + expect(camera.cameraPermissionsRequested, isTrue); + expect(camera.startedListeningForDeviceOrientationChanges, isTrue); + + // Verify CameraSelector is set with appropriate lens direction. + expect(camera.cameraSelector, equals(camera.mockBackCameraSelector)); + + // Verify the camera's Preview instance is instantiated properly. + expect(camera.preview, equals(camera.testPreview)); + + // Verify the camera's Preview instance has its surface provider set. + verify(camera.preview!.setSurfaceProvider()); + }); + + test( + 'initializeCamera throws AssertionError when createCamera has not been called before initializedCamera', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + expect(() => camera.initializeCamera(3), throwsAssertionError); + }); + + test('initializeCamera sends expected CameraInitializedEvent', () async { + final MockAndroidCameraCamerax camera = MockAndroidCameraCamerax(); + camera.processCameraProvider = MockProcessCameraProvider(); + const int cameraId = 10; + const CameraLensDirection testLensDirection = CameraLensDirection.back; + const int testSensorOrientation = 90; + const CameraDescription testCameraDescription = CameraDescription( + name: 'cameraName', + lensDirection: testLensDirection, + sensorOrientation: testSensorOrientation); + const ResolutionPreset testResolutionPreset = ResolutionPreset.veryHigh; + const bool enableAudio = true; + const int resolutionWidth = 350; + const int resolutionHeight = 750; + final Camera mockCamera = MockCamera(); + final ResolutionInfo testResolutionInfo = + ResolutionInfo(width: resolutionWidth, height: resolutionHeight); + + // TODO(camsim99): Modify this when camera configuration is supported and + // defualt values no longer being used. + // https://github.com/flutter/flutter/issues/120468 + // https://github.com/flutter/flutter/issues/120467 + final CameraInitializedEvent testCameraInitializedEvent = + CameraInitializedEvent( + cameraId, + resolutionWidth.toDouble(), + resolutionHeight.toDouble(), + ExposureMode.auto, + false, + FocusMode.auto, + false); + + // Call createCamera. + when(camera.testPreview.setSurfaceProvider()) + .thenAnswer((_) async => cameraId); + await camera.createCamera(testCameraDescription, testResolutionPreset, + enableAudio: enableAudio); + + when(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.testPreview])) + .thenAnswer((_) async => mockCamera); + when(camera.testPreview.getResolutionInfo()) + .thenAnswer((_) async => testResolutionInfo); + + // Start listening to camera events stream to verify the proper CameraInitializedEvent is sent. + camera.cameraEventStreamController.stream.listen((CameraEvent event) { + expect(event, const TypeMatcher()); + expect(event, equals(testCameraInitializedEvent)); + }); + + await camera.initializeCamera(cameraId); + + // Verify preview was bound and unbound to get preview resolution information. + verify(camera.processCameraProvider!.bindToLifecycle( + camera.cameraSelector!, [camera.testPreview])); + verify(camera.processCameraProvider!.unbind([camera.testPreview])); + + // Check camera instance was received, but preview is no longer bound. + expect(camera.camera, equals(mockCamera)); + expect(camera.previewIsBound, isFalse); + }); + + test('dispose releases Flutter surface texture and unbinds all use cases', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.preview = MockPreview(); + camera.processCameraProvider = MockProcessCameraProvider(); + + camera.dispose(3); + + verify(camera.preview!.releaseFlutterSurfaceTexture()); + verify(camera.processCameraProvider!.unbindAll()); + }); + + test('onCameraInitialized stream emits CameraInitializedEvents', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 16; + final Stream eventStream = + camera.onCameraInitialized(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + const CameraInitializedEvent testEvent = CameraInitializedEvent( + cameraId, 320, 80, ExposureMode.auto, false, FocusMode.auto, false); + + camera.cameraEventStreamController.add(testEvent); + + expect(await streamQueue.next, testEvent); + await streamQueue.cancel(); + }); + + test('onCameraError stream emits errors caught by system services', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 27; + const String testErrorDescription = 'Test error description!'; + final Stream eventStream = camera.onCameraError(cameraId); + final StreamQueue streamQueue = + StreamQueue(eventStream); + + SystemServices.cameraErrorStreamController.add(testErrorDescription); + + expect(await streamQueue.next, + equals(const CameraErrorEvent(cameraId, testErrorDescription))); + await streamQueue.cancel(); + }); + + test( + 'onDeviceOrientationChanged stream emits changes in device oreintation detected by system services', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final Stream eventStream = + camera.onDeviceOrientationChanged(); + final StreamQueue streamQueue = + StreamQueue(eventStream); + const DeviceOrientationChangedEvent testEvent = + DeviceOrientationChangedEvent(DeviceOrientation.portraitDown); + + SystemServices.deviceOrientationChangedStreamController.add(testEvent); + + expect(await streamQueue.next, testEvent); + await streamQueue.cancel(); + }); + + test( + 'pausePreview unbinds preview from lifecycle when preview is nonnull and has been bound to lifecycle', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.preview = MockPreview(); + camera.previewIsBound = true; + + await camera.pausePreview(579); + + verify(camera.processCameraProvider!.unbind([camera.preview!])); + expect(camera.previewIsBound, isFalse); + }); + + test( + 'pausePreview does not unbind preview from lifecycle when preview has not been bound to lifecycle', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.preview = MockPreview(); + + await camera.pausePreview(632); + + verifyNever( + camera.processCameraProvider!.unbind([camera.preview!])); + }); + + test('resumePreview does not bind preview to lifecycle if already bound', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + camera.previewIsBound = true; + + await camera.resumePreview(78); + + verifyNever(camera.processCameraProvider! + .bindToLifecycle(camera.cameraSelector!, [camera.preview!])); + }); + + test('resumePreview binds preview to lifecycle if not already bound', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + + await camera.resumePreview(78); + + verify(camera.processCameraProvider! + .bindToLifecycle(camera.cameraSelector!, [camera.preview!])); + }); + + test( + 'buildPreview returns a FutureBuilder that does not return a Texture until the preview is bound to the lifecycle', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int textureId = 75; + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + + final FutureBuilder previewWidget = + camera.buildPreview(textureId) as FutureBuilder; + + expect( + previewWidget.builder( + MockBuildContext(), const AsyncSnapshot.nothing()), + isA()); + expect( + previewWidget.builder( + MockBuildContext(), const AsyncSnapshot.waiting()), + isA()); + expect( + previewWidget.builder(MockBuildContext(), + const AsyncSnapshot.withData(ConnectionState.active, null)), + isA()); + }); + + test( + 'buildPreview returns a FutureBuilder that returns a Texture once the preview is bound to the lifecycle', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int textureId = 75; + + camera.processCameraProvider = MockProcessCameraProvider(); + camera.cameraSelector = MockCameraSelector(); + camera.preview = MockPreview(); + + final FutureBuilder previewWidget = + camera.buildPreview(textureId) as FutureBuilder; + + final Texture previewTexture = previewWidget.builder(MockBuildContext(), + const AsyncSnapshot.withData(ConnectionState.done, null)) + as Texture; + expect(previewTexture.textureId, equals(textureId)); + }); +} + +/// Mock of [AndroidCameraCameraX] that stubs behavior of some methods for +/// testing. +class MockAndroidCameraCamerax extends AndroidCameraCameraX { + bool cameraPermissionsRequested = false; + bool startedListeningForDeviceOrientationChanges = false; + final MockPreview testPreview = MockPreview(); + final MockCameraSelector mockBackCameraSelector = MockCameraSelector(); + final MockCameraSelector mockFrontCameraSelector = MockCameraSelector(); + + @override + Future requestCameraPermissions(bool enableAudio) async { + cameraPermissionsRequested = true; + } + + @override + void startListeningForDeviceOrientationChange( + bool cameraIsFrontFacing, int sensorOrientation) { + startedListeningForDeviceOrientationChanges = true; + return; + } + + @override + CameraSelector createCameraSelector(int cameraSelectorLensDirection) { + switch (cameraSelectorLensDirection) { + case CameraSelector.lensFacingFront: + return mockFrontCameraSelector; + case CameraSelector.lensFacingBack: + default: + return mockBackCameraSelector; + } + } + + @override + Preview createPreview(int targetRotation, ResolutionInfo? targetResolution) { + return testPreview; + } +} diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart new file mode 100644 index 000000000000..af225a10c64a --- /dev/null +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart @@ -0,0 +1,389 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in camera_android_camerax/test/android_camera_camerax_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i8; + +import 'package:camera_android_camerax/src/camera.dart' as _i3; +import 'package:camera_android_camerax/src/camera_info.dart' as _i7; +import 'package:camera_android_camerax/src/camera_selector.dart' as _i9; +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i2; +import 'package:camera_android_camerax/src/preview.dart' as _i10; +import 'package:camera_android_camerax/src/process_camera_provider.dart' + as _i11; +import 'package:camera_android_camerax/src/use_case.dart' as _i12; +import 'package:flutter/foundation.dart' as _i6; +import 'package:flutter/services.dart' as _i5; +import 'package:flutter/src/widgets/framework.dart' as _i4; +import 'package:flutter/src/widgets/notification_listener.dart' as _i13; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResolutionInfo_0 extends _i1.SmartFake + implements _i2.ResolutionInfo { + _FakeResolutionInfo_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeCamera_1 extends _i1.SmartFake implements _i3.Camera { + _FakeCamera_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeWidget_2 extends _i1.SmartFake implements _i4.Widget { + _FakeWidget_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeInheritedWidget_3 extends _i1.SmartFake + implements _i4.InheritedWidget { + _FakeInheritedWidget_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({_i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info}) => + super.toString(); +} + +class _FakeDiagnosticsNode_4 extends _i1.SmartFake + implements _i6.DiagnosticsNode { + _FakeDiagnosticsNode_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); + + @override + String toString({ + _i6.TextTreeConfiguration? parentConfiguration, + _i5.DiagnosticLevel? minLevel = _i5.DiagnosticLevel.info, + }) => + super.toString(); +} + +/// A class which mocks [Camera]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCamera extends _i1.Mock implements _i3.Camera {} + +/// A class which mocks [CameraInfo]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCameraInfo extends _i1.Mock implements _i7.CameraInfo { + @override + _i8.Future getSensorRotationDegrees() => (super.noSuchMethod( + Invocation.method( + #getSensorRotationDegrees, + [], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); +} + +/// A class which mocks [CameraSelector]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCameraSelector extends _i1.Mock implements _i9.CameraSelector { + @override + _i8.Future> filter(List<_i7.CameraInfo>? cameraInfos) => + (super.noSuchMethod( + Invocation.method( + #filter, + [cameraInfos], + ), + returnValue: _i8.Future>.value(<_i7.CameraInfo>[]), + returnValueForMissingStub: + _i8.Future>.value(<_i7.CameraInfo>[]), + ) as _i8.Future>); +} + +/// A class which mocks [Preview]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPreview extends _i1.Mock implements _i10.Preview { + @override + _i8.Future setSurfaceProvider() => (super.noSuchMethod( + Invocation.method( + #setSurfaceProvider, + [], + ), + returnValue: _i8.Future.value(0), + returnValueForMissingStub: _i8.Future.value(0), + ) as _i8.Future); + @override + void releaseFlutterSurfaceTexture() => super.noSuchMethod( + Invocation.method( + #releaseFlutterSurfaceTexture, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i8.Future<_i2.ResolutionInfo> getResolutionInfo() => (super.noSuchMethod( + Invocation.method( + #getResolutionInfo, + [], + ), + returnValue: _i8.Future<_i2.ResolutionInfo>.value(_FakeResolutionInfo_0( + this, + Invocation.method( + #getResolutionInfo, + [], + ), + )), + returnValueForMissingStub: + _i8.Future<_i2.ResolutionInfo>.value(_FakeResolutionInfo_0( + this, + Invocation.method( + #getResolutionInfo, + [], + ), + )), + ) as _i8.Future<_i2.ResolutionInfo>); +} + +/// A class which mocks [ProcessCameraProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockProcessCameraProvider extends _i1.Mock + implements _i11.ProcessCameraProvider { + @override + _i8.Future> getAvailableCameraInfos() => + (super.noSuchMethod( + Invocation.method( + #getAvailableCameraInfos, + [], + ), + returnValue: _i8.Future>.value(<_i7.CameraInfo>[]), + returnValueForMissingStub: + _i8.Future>.value(<_i7.CameraInfo>[]), + ) as _i8.Future>); + @override + _i8.Future<_i3.Camera> bindToLifecycle( + _i9.CameraSelector? cameraSelector, + List<_i12.UseCase>? useCases, + ) => + (super.noSuchMethod( + Invocation.method( + #bindToLifecycle, + [ + cameraSelector, + useCases, + ], + ), + returnValue: _i8.Future<_i3.Camera>.value(_FakeCamera_1( + this, + Invocation.method( + #bindToLifecycle, + [ + cameraSelector, + useCases, + ], + ), + )), + returnValueForMissingStub: _i8.Future<_i3.Camera>.value(_FakeCamera_1( + this, + Invocation.method( + #bindToLifecycle, + [ + cameraSelector, + useCases, + ], + ), + )), + ) as _i8.Future<_i3.Camera>); + @override + void unbind(List<_i12.UseCase>? useCases) => super.noSuchMethod( + Invocation.method( + #unbind, + [useCases], + ), + returnValueForMissingStub: null, + ); + @override + void unbindAll() => super.noSuchMethod( + Invocation.method( + #unbindAll, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [BuildContext]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockBuildContext extends _i1.Mock implements _i4.BuildContext { + MockBuildContext() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Widget get widget => (super.noSuchMethod( + Invocation.getter(#widget), + returnValue: _FakeWidget_2( + this, + Invocation.getter(#widget), + ), + ) as _i4.Widget); + @override + bool get mounted => (super.noSuchMethod( + Invocation.getter(#mounted), + returnValue: false, + ) as bool); + @override + bool get debugDoingBuild => (super.noSuchMethod( + Invocation.getter(#debugDoingBuild), + returnValue: false, + ) as bool); + @override + _i4.InheritedWidget dependOnInheritedElement( + _i4.InheritedElement? ancestor, { + Object? aspect, + }) => + (super.noSuchMethod( + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + returnValue: _FakeInheritedWidget_3( + this, + Invocation.method( + #dependOnInheritedElement, + [ancestor], + {#aspect: aspect}, + ), + ), + ) as _i4.InheritedWidget); + @override + void visitAncestorElements(bool Function(_i4.Element)? visitor) => + super.noSuchMethod( + Invocation.method( + #visitAncestorElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void visitChildElements(_i4.ElementVisitor? visitor) => super.noSuchMethod( + Invocation.method( + #visitChildElements, + [visitor], + ), + returnValueForMissingStub: null, + ); + @override + void dispatchNotification(_i13.Notification? notification) => + super.noSuchMethod( + Invocation.method( + #dispatchNotification, + [notification], + ), + returnValueForMissingStub: null, + ); + @override + _i6.DiagnosticsNode describeElement( + String? name, { + _i6.DiagnosticsTreeStyle? style = _i6.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_4( + this, + Invocation.method( + #describeElement, + [name], + {#style: style}, + ), + ), + ) as _i6.DiagnosticsNode); + @override + _i6.DiagnosticsNode describeWidget( + String? name, { + _i6.DiagnosticsTreeStyle? style = _i6.DiagnosticsTreeStyle.errorProperty, + }) => + (super.noSuchMethod( + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + returnValue: _FakeDiagnosticsNode_4( + this, + Invocation.method( + #describeWidget, + [name], + {#style: style}, + ), + ), + ) as _i6.DiagnosticsNode); + @override + List<_i6.DiagnosticsNode> describeMissingAncestor( + {required Type? expectedAncestorType}) => + (super.noSuchMethod( + Invocation.method( + #describeMissingAncestor, + [], + {#expectedAncestorType: expectedAncestorType}, + ), + returnValue: <_i6.DiagnosticsNode>[], + ) as List<_i6.DiagnosticsNode>); + @override + _i6.DiagnosticsNode describeOwnershipChain(String? name) => + (super.noSuchMethod( + Invocation.method( + #describeOwnershipChain, + [name], + ), + returnValue: _FakeDiagnosticsNode_4( + this, + Invocation.method( + #describeOwnershipChain, + [name], + ), + ), + ) as _i6.DiagnosticsNode); +} diff --git a/packages/camera/camera_android_camerax/test/camera_info_test.dart b/packages/camera/camera_android_camerax/test/camera_info_test.dart index eda822b33f73..852c799ebfbe 100644 --- a/packages/camera/camera_android_camerax/test/camera_info_test.dart +++ b/packages/camera/camera_android_camerax/test/camera_info_test.dart @@ -3,14 +3,14 @@ // found in the LICENSE file. import 'package:camera_android_camerax/src/camera_info.dart'; -import 'package:camera_android_camerax/src/camerax_library.pigeon.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'camera_info_test.mocks.dart'; -import 'test_camerax_library.pigeon.dart'; +import 'test_camerax_library.g.dart'; @GenerateMocks([TestCameraInfoHostApi]) void main() { diff --git a/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart index e1f1e3ca9e9b..5e558a8226b6 100644 --- a/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/camera_info_test.mocks.dart @@ -1,11 +1,11 @@ -// Mocks generated by Mockito 5.3.0 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in camera_android_camerax/test/camera_info_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.pigeon.dart' as _i2; +import 'test_camerax_library.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -29,6 +29,10 @@ class MockTestCameraInfoHostApi extends _i1.Mock @override int getSensorRotationDegrees(int? identifier) => (super.noSuchMethod( - Invocation.method(#getSensorRotationDegrees, [identifier]), - returnValue: 0) as int); + Invocation.method( + #getSensorRotationDegrees, + [identifier], + ), + returnValue: 0, + ) as int); } diff --git a/packages/camera/camera_android_camerax/test/camera_selector_test.dart b/packages/camera/camera_android_camerax/test/camera_selector_test.dart index c4ccd6262376..52f9a18d956e 100644 --- a/packages/camera/camera_android_camerax/test/camera_selector_test.dart +++ b/packages/camera/camera_android_camerax/test/camera_selector_test.dart @@ -4,14 +4,14 @@ import 'package:camera_android_camerax/src/camera_info.dart'; import 'package:camera_android_camerax/src/camera_selector.dart'; -import 'package:camera_android_camerax/src/camerax_library.pigeon.dart'; +import 'package:camera_android_camerax/src/camerax_library.g.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'camera_selector_test.mocks.dart'; -import 'test_camerax_library.pigeon.dart'; +import 'test_camerax_library.g.dart'; @GenerateMocks([TestCameraSelectorHostApi]) void main() { @@ -60,10 +60,10 @@ void main() { ); CameraSelector( instanceManager: instanceManager, - lensFacing: CameraSelector.LENS_FACING_BACK); + lensFacing: CameraSelector.lensFacingBack); verify( - mockApi.create(argThat(isA()), CameraSelector.LENS_FACING_BACK)); + mockApi.create(argThat(isA()), CameraSelector.lensFacingBack)); }); test('filterTest', () async { @@ -108,14 +108,14 @@ void main() { instanceManager: instanceManager, ); - flutterApi.create(0, CameraSelector.LENS_FACING_BACK); + flutterApi.create(0, CameraSelector.lensFacingBack); expect(instanceManager.getInstanceWithWeakReference(0), isA()); expect( (instanceManager.getInstanceWithWeakReference(0)! as CameraSelector) .lensFacing, - equals(CameraSelector.LENS_FACING_BACK)); + equals(CameraSelector.lensFacingBack)); }); }); } diff --git a/packages/camera/camera_android_camerax/test/camera_selector_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera_selector_test.mocks.dart index 456db1eaf822..31dce5177e2d 100644 --- a/packages/camera/camera_android_camerax/test/camera_selector_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/camera_selector_test.mocks.dart @@ -1,11 +1,11 @@ -// Mocks generated by Mockito 5.3.0 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in camera_android_camerax/test/camera_selector_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.pigeon.dart' as _i2; +import 'test_camerax_library.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -28,11 +28,33 @@ class MockTestCameraSelectorHostApi extends _i1.Mock } @override - void create(int? identifier, int? lensFacing) => - super.noSuchMethod(Invocation.method(#create, [identifier, lensFacing]), - returnValueForMissingStub: null); + void create( + int? identifier, + int? lensFacing, + ) => + super.noSuchMethod( + Invocation.method( + #create, + [ + identifier, + lensFacing, + ], + ), + returnValueForMissingStub: null, + ); @override - List filter(int? identifier, List? cameraInfoIds) => (super - .noSuchMethod(Invocation.method(#filter, [identifier, cameraInfoIds]), - returnValue: []) as List); + List filter( + int? identifier, + List? cameraInfoIds, + ) => + (super.noSuchMethod( + Invocation.method( + #filter, + [ + identifier, + cameraInfoIds, + ], + ), + returnValue: [], + ) as List); } diff --git a/packages/camera/camera_android_camerax/test/camera_test.dart b/packages/camera/camera_android_camerax/test/camera_test.dart new file mode 100644 index 000000000000..c2948282dcf1 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/camera_test.dart @@ -0,0 +1,26 @@ +// 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:camera_android_camerax/src/camera.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Camera', () { + test('flutterApiCreateTest', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final CameraFlutterApiImpl flutterApi = CameraFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create(0); + + expect(instanceManager.getInstanceWithWeakReference(0), isA()); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/preview_test.dart b/packages/camera/camera_android_camerax/test/preview_test.dart new file mode 100644 index 000000000000..36b56f0046e1 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/preview_test.dart @@ -0,0 +1,138 @@ +// 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:camera_android_camerax/src/camerax_library.g.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/preview.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'preview_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestPreviewHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Preview', () { + tearDown(() => TestPreviewHostApi.setup(null)); + + test('detached create does not call create on the Java side', () async { + final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); + TestPreviewHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + Preview.detached( + instanceManager: instanceManager, + targetRotation: 90, + targetResolution: ResolutionInfo(width: 50, height: 10), + ); + + verifyNever(mockApi.create(argThat(isA()), argThat(isA()), + argThat(isA()))); + }); + + test('create calls create on the Java side', () async { + final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); + TestPreviewHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + const int targetRotation = 90; + const int targetResolutionWidth = 10; + const int targetResolutionHeight = 50; + Preview( + instanceManager: instanceManager, + targetRotation: targetRotation, + targetResolution: ResolutionInfo( + width: targetResolutionWidth, height: targetResolutionHeight), + ); + + final VerificationResult createVerification = verify(mockApi.create( + argThat(isA()), argThat(equals(targetRotation)), captureAny)); + final ResolutionInfo capturedResolutionInfo = + createVerification.captured.single as ResolutionInfo; + expect(capturedResolutionInfo.width, equals(targetResolutionWidth)); + expect(capturedResolutionInfo.height, equals(targetResolutionHeight)); + }); + + test( + 'setSurfaceProvider makes call to set surface provider for preview instance', + () async { + final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); + TestPreviewHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + const int textureId = 8; + final Preview preview = Preview.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance( + preview, + 0, + onCopy: (_) => Preview.detached(), + ); + + when(mockApi.setSurfaceProvider(instanceManager.getIdentifier(preview))) + .thenReturn(textureId); + expect(await preview.setSurfaceProvider(), equals(textureId)); + + verify( + mockApi.setSurfaceProvider(instanceManager.getIdentifier(preview))); + }); + + test( + 'releaseFlutterSurfaceTexture makes call to relase flutter surface texture entry', + () async { + final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); + TestPreviewHostApi.setup(mockApi); + + final Preview preview = Preview.detached(); + + preview.releaseFlutterSurfaceTexture(); + + verify(mockApi.releaseFlutterSurfaceTexture()); + }); + + test( + 'getResolutionInfo makes call to get resolution information for preview instance', + () async { + final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); + TestPreviewHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final Preview preview = Preview.detached( + instanceManager: instanceManager, + ); + const int resolutionWidth = 10; + const int resolutionHeight = 60; + final ResolutionInfo testResolutionInfo = + ResolutionInfo(width: resolutionWidth, height: resolutionHeight); + + instanceManager.addHostCreatedInstance( + preview, + 0, + onCopy: (_) => Preview.detached(), + ); + + when(mockApi.getResolutionInfo(instanceManager.getIdentifier(preview))) + .thenReturn(testResolutionInfo); + + final ResolutionInfo previewResolutionInfo = + await preview.getResolutionInfo(); + expect(previewResolutionInfo.width, equals(resolutionWidth)); + expect(previewResolutionInfo.height, equals(resolutionHeight)); + + verify(mockApi.getResolutionInfo(instanceManager.getIdentifier(preview))); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/preview_test.mocks.dart b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart new file mode 100644 index 000000000000..60fa1527487b --- /dev/null +++ b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart @@ -0,0 +1,89 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in camera_android_camerax/test/preview_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i3; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResolutionInfo_0 extends _i1.SmartFake + implements _i2.ResolutionInfo { + _FakeResolutionInfo_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [TestPreviewHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestPreviewHostApi extends _i1.Mock + implements _i3.TestPreviewHostApi { + MockTestPreviewHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void create( + int? identifier, + int? rotation, + _i2.ResolutionInfo? targetResolution, + ) => + super.noSuchMethod( + Invocation.method( + #create, + [ + identifier, + rotation, + targetResolution, + ], + ), + returnValueForMissingStub: null, + ); + @override + int setSurfaceProvider(int? identifier) => (super.noSuchMethod( + Invocation.method( + #setSurfaceProvider, + [identifier], + ), + returnValue: 0, + ) as int); + @override + void releaseFlutterSurfaceTexture() => super.noSuchMethod( + Invocation.method( + #releaseFlutterSurfaceTexture, + [], + ), + returnValueForMissingStub: null, + ); + @override + _i2.ResolutionInfo getResolutionInfo(int? identifier) => (super.noSuchMethod( + Invocation.method( + #getResolutionInfo, + [identifier], + ), + returnValue: _FakeResolutionInfo_0( + this, + Invocation.method( + #getResolutionInfo, + [identifier], + ), + ), + ) as _i2.ResolutionInfo); +} diff --git a/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart b/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart index 65e7d00ddaea..548ac3e00d65 100644 --- a/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart +++ b/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart @@ -2,15 +2,18 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:camera_android_camerax/src/camera.dart'; import 'package:camera_android_camerax/src/camera_info.dart'; +import 'package:camera_android_camerax/src/camera_selector.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:camera_android_camerax/src/process_camera_provider.dart'; +import 'package:camera_android_camerax/src/use_case.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'process_camera_provider_test.mocks.dart'; -import 'test_camerax_library.pigeon.dart'; +import 'test_camerax_library.g.dart'; @GenerateMocks([TestProcessCameraProviderHostApi]) void main() { @@ -78,6 +81,114 @@ void main() { verify(mockApi.getAvailableCameraInfos(0)); }); + test('bindToLifecycleTest', () async { + final MockTestProcessCameraProviderHostApi mockApi = + MockTestProcessCameraProviderHostApi(); + TestProcessCameraProviderHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ProcessCameraProvider processCameraProvider = + ProcessCameraProvider.detached( + instanceManager: instanceManager, + ); + final CameraSelector fakeCameraSelector = + CameraSelector.detached(instanceManager: instanceManager); + final UseCase fakeUseCase = + UseCase.detached(instanceManager: instanceManager); + final Camera fakeCamera = + Camera.detached(instanceManager: instanceManager); + + instanceManager.addHostCreatedInstance( + processCameraProvider, + 0, + onCopy: (_) => ProcessCameraProvider.detached(), + ); + instanceManager.addHostCreatedInstance( + fakeCameraSelector, + 1, + onCopy: (_) => CameraSelector.detached(), + ); + instanceManager.addHostCreatedInstance( + fakeUseCase, + 2, + onCopy: (_) => UseCase.detached(), + ); + instanceManager.addHostCreatedInstance( + fakeCamera, + 3, + onCopy: (_) => Camera.detached(), + ); + + when(mockApi.bindToLifecycle(0, 1, [2])).thenReturn(3); + expect( + await processCameraProvider + .bindToLifecycle(fakeCameraSelector, [fakeUseCase]), + equals(fakeCamera)); + verify(mockApi.bindToLifecycle(0, 1, [2])); + }); + + test('unbindTest', () async { + final MockTestProcessCameraProviderHostApi mockApi = + MockTestProcessCameraProviderHostApi(); + TestProcessCameraProviderHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ProcessCameraProvider processCameraProvider = + ProcessCameraProvider.detached( + instanceManager: instanceManager, + ); + final UseCase fakeUseCase = + UseCase.detached(instanceManager: instanceManager); + + instanceManager.addHostCreatedInstance( + processCameraProvider, + 0, + onCopy: (_) => ProcessCameraProvider.detached(), + ); + instanceManager.addHostCreatedInstance( + fakeUseCase, + 1, + onCopy: (_) => UseCase.detached(), + ); + + processCameraProvider.unbind([fakeUseCase]); + verify(mockApi.unbind(0, [1])); + }); + + test('unbindAllTest', () async { + final MockTestProcessCameraProviderHostApi mockApi = + MockTestProcessCameraProviderHostApi(); + TestProcessCameraProviderHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ProcessCameraProvider processCameraProvider = + ProcessCameraProvider.detached( + instanceManager: instanceManager, + ); + final UseCase fakeUseCase = + UseCase.detached(instanceManager: instanceManager); + + instanceManager.addHostCreatedInstance( + processCameraProvider, + 0, + onCopy: (_) => ProcessCameraProvider.detached(), + ); + instanceManager.addHostCreatedInstance( + fakeUseCase, + 1, + onCopy: (_) => UseCase.detached(), + ); + + processCameraProvider.unbind([fakeUseCase]); + verify(mockApi.unbind(0, [1])); + }); + test('flutterApiCreateTest', () { final InstanceManager instanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, diff --git a/packages/camera/camera_android_camerax/test/process_camera_provider_test.mocks.dart b/packages/camera/camera_android_camerax/test/process_camera_provider_test.mocks.dart index 9fcfe690c062..2ce4ab72fa57 100644 --- a/packages/camera/camera_android_camerax/test/process_camera_provider_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/process_camera_provider_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.3.0 from annotations +// Mocks generated by Mockito 5.3.2 from annotations // in camera_android_camerax/test/process_camera_provider_test.dart. // Do not manually edit this file. @@ -7,7 +7,7 @@ import 'dart:async' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'test_camerax_library.pigeon.dart' as _i2; +import 'test_camerax_library.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -30,11 +30,59 @@ class MockTestProcessCameraProviderHostApi extends _i1.Mock } @override - _i3.Future getInstance() => - (super.noSuchMethod(Invocation.method(#getInstance, []), - returnValue: _i3.Future.value(0)) as _i3.Future); + _i3.Future getInstance() => (super.noSuchMethod( + Invocation.method( + #getInstance, + [], + ), + returnValue: _i3.Future.value(0), + ) as _i3.Future); @override List getAvailableCameraInfos(int? identifier) => (super.noSuchMethod( - Invocation.method(#getAvailableCameraInfos, [identifier]), - returnValue: []) as List); + Invocation.method( + #getAvailableCameraInfos, + [identifier], + ), + returnValue: [], + ) as List); + @override + int bindToLifecycle( + int? identifier, + int? cameraSelectorIdentifier, + List? useCaseIds, + ) => + (super.noSuchMethod( + Invocation.method( + #bindToLifecycle, + [ + identifier, + cameraSelectorIdentifier, + useCaseIds, + ], + ), + returnValue: 0, + ) as int); + @override + void unbind( + int? identifier, + List? useCaseIds, + ) => + super.noSuchMethod( + Invocation.method( + #unbind, + [ + identifier, + useCaseIds, + ], + ), + returnValueForMissingStub: null, + ); + @override + void unbindAll(int? identifier) => super.noSuchMethod( + Invocation.method( + #unbindAll, + [identifier], + ), + returnValueForMissingStub: null, + ); } diff --git a/packages/camera/camera_android_camerax/test/system_services_test.dart b/packages/camera/camera_android_camerax/test/system_services_test.dart new file mode 100644 index 000000000000..38037eaa135c --- /dev/null +++ b/packages/camera/camera_android_camerax/test/system_services_test.dart @@ -0,0 +1,110 @@ +// 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:camera_android_camerax/src/camerax_library.g.dart' + show CameraPermissionsErrorData; +import 'package:camera_android_camerax/src/system_services.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart' + show CameraException, DeviceOrientationChangedEvent; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'system_services_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks([TestSystemServicesHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('SystemServices', () { + tearDown(() => TestProcessCameraProviderHostApi.setup(null)); + + test( + 'requestCameraPermissionsFromInstance completes normally without errors test', + () async { + final MockTestSystemServicesHostApi mockApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockApi); + + when(mockApi.requestCameraPermissions(true)) + .thenAnswer((_) async => null); + + await SystemServices.requestCameraPermissions(true); + verify(mockApi.requestCameraPermissions(true)); + }); + + test( + 'requestCameraPermissionsFromInstance throws CameraException if there was a request error', + () { + final MockTestSystemServicesHostApi mockApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockApi); + final CameraPermissionsErrorData error = CameraPermissionsErrorData( + errorCode: 'Test error code', + description: 'Test error description', + ); + + when(mockApi.requestCameraPermissions(true)) + .thenAnswer((_) async => error); + + expect( + () async => SystemServices.requestCameraPermissions(true), + throwsA(isA() + .having((CameraException e) => e.code, 'code', 'Test error code') + .having((CameraException e) => e.description, 'description', + 'Test error description'))); + verify(mockApi.requestCameraPermissions(true)); + }); + + test('startListeningForDeviceOrientationChangeTest', () async { + final MockTestSystemServicesHostApi mockApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockApi); + + SystemServices.startListeningForDeviceOrientationChange(true, 90); + verify(mockApi.startListeningForDeviceOrientationChange(true, 90)); + }); + + test('stopListeningForDeviceOrientationChangeTest', () async { + final MockTestSystemServicesHostApi mockApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockApi); + + SystemServices.stopListeningForDeviceOrientationChange(); + verify(mockApi.stopListeningForDeviceOrientationChange()); + }); + + test('onDeviceOrientationChanged adds new orientation to stream', () { + SystemServices.deviceOrientationChangedStreamController.stream + .listen((DeviceOrientationChangedEvent event) { + expect(event.orientation, equals(DeviceOrientation.landscapeLeft)); + }); + SystemServicesFlutterApiImpl() + .onDeviceOrientationChanged('LANDSCAPE_LEFT'); + }); + + test( + 'onDeviceOrientationChanged throws error if new orientation is invalid', + () { + expect( + () => SystemServicesFlutterApiImpl() + .onDeviceOrientationChanged('FAKE_ORIENTATION'), + throwsA(isA().having( + (ArgumentError e) => e.message, + 'message', + '"FAKE_ORIENTATION" is not a valid DeviceOrientation value'))); + }); + + test('onCameraError adds new error to stream', () { + const String testErrorDescription = 'Test error description!'; + SystemServices.cameraErrorStreamController.stream + .listen((String errorDescription) { + expect(errorDescription, equals(testErrorDescription)); + }); + SystemServicesFlutterApiImpl().onCameraError(testErrorDescription); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart new file mode 100644 index 000000000000..0963ffb26a2a --- /dev/null +++ b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart @@ -0,0 +1,66 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in camera_android_camerax/test/system_services_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:camera_android_camerax/src/camerax_library.g.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [TestSystemServicesHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestSystemServicesHostApi extends _i1.Mock + implements _i2.TestSystemServicesHostApi { + MockTestSystemServicesHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future<_i4.CameraPermissionsErrorData?> requestCameraPermissions( + bool? enableAudio) => + (super.noSuchMethod( + Invocation.method( + #requestCameraPermissions, + [enableAudio], + ), + returnValue: _i3.Future<_i4.CameraPermissionsErrorData?>.value(), + ) as _i3.Future<_i4.CameraPermissionsErrorData?>); + @override + void startListeningForDeviceOrientationChange( + bool? isFrontFacing, + int? sensorOrientation, + ) => + super.noSuchMethod( + Invocation.method( + #startListeningForDeviceOrientationChange, + [ + isFrontFacing, + sensorOrientation, + ], + ), + returnValueForMissingStub: null, + ); + @override + void stopListeningForDeviceOrientationChange() => super.noSuchMethod( + Invocation.method( + #stopListeningForDeviceOrientationChange, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart new file mode 100644 index 000000000000..3f0e9c2d38a5 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart @@ -0,0 +1,475 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v3.2.9), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import +// ignore_for_file: avoid_relative_lib_imports +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:camera_android_camerax/src/camerax_library.g.dart'; + +class _TestJavaObjectHostApiCodec extends StandardMessageCodec { + const _TestJavaObjectHostApiCodec(); +} + +abstract class TestJavaObjectHostApi { + static const MessageCodec codec = _TestJavaObjectHostApiCodec(); + + void dispose(int identifier); + static void setup(TestJavaObjectHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.JavaObjectHostApi.dispose', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.JavaObjectHostApi.dispose was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.JavaObjectHostApi.dispose was null, expected non-null int.'); + api.dispose(arg_identifier!); + return {}; + }); + } + } + } +} + +class _TestCameraInfoHostApiCodec extends StandardMessageCodec { + const _TestCameraInfoHostApiCodec(); +} + +abstract class TestCameraInfoHostApi { + static const MessageCodec codec = _TestCameraInfoHostApiCodec(); + + int getSensorRotationDegrees(int identifier); + static void setup(TestCameraInfoHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees was null, expected non-null int.'); + final int output = api.getSensorRotationDegrees(arg_identifier!); + return {'result': output}; + }); + } + } + } +} + +class _TestCameraSelectorHostApiCodec extends StandardMessageCodec { + const _TestCameraSelectorHostApiCodec(); +} + +abstract class TestCameraSelectorHostApi { + static const MessageCodec codec = _TestCameraSelectorHostApiCodec(); + + void create(int identifier, int? lensFacing); + List filter(int identifier, List cameraInfoIds); + static void setup(TestCameraSelectorHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraSelectorHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.create was null, expected non-null int.'); + final int? arg_lensFacing = (args[1] as int?); + api.create(arg_identifier!, arg_lensFacing); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraSelectorHostApi.filter', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.filter was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.filter was null, expected non-null int.'); + final List? arg_cameraInfoIds = + (args[1] as List?)?.cast(); + assert(arg_cameraInfoIds != null, + 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.filter was null, expected non-null List.'); + final List output = + api.filter(arg_identifier!, arg_cameraInfoIds!); + return {'result': output}; + }); + } + } + } +} + +class _TestProcessCameraProviderHostApiCodec extends StandardMessageCodec { + const _TestProcessCameraProviderHostApiCodec(); +} + +abstract class TestProcessCameraProviderHostApi { + static const MessageCodec codec = + _TestProcessCameraProviderHostApiCodec(); + + Future getInstance(); + List getAvailableCameraInfos(int identifier); + int bindToLifecycle( + int identifier, int cameraSelectorIdentifier, List useCaseIds); + void unbind(int identifier, List useCaseIds); + void unbindAll(int identifier); + static void setup(TestProcessCameraProviderHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final int output = await api.getInstance(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos was null, expected non-null int.'); + final List output = + api.getAvailableCameraInfos(arg_identifier!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle was null, expected non-null int.'); + final int? arg_cameraSelectorIdentifier = (args[1] as int?); + assert(arg_cameraSelectorIdentifier != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle was null, expected non-null int.'); + final List? arg_useCaseIds = + (args[2] as List?)?.cast(); + assert(arg_useCaseIds != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.bindToLifecycle was null, expected non-null List.'); + final int output = api.bindToLifecycle( + arg_identifier!, arg_cameraSelectorIdentifier!, arg_useCaseIds!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind was null, expected non-null int.'); + final List? arg_useCaseIds = + (args[1] as List?)?.cast(); + assert(arg_useCaseIds != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.unbind was null, expected non-null List.'); + api.unbind(arg_identifier!, arg_useCaseIds!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.unbindAll', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.unbindAll was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.unbindAll was null, expected non-null int.'); + api.unbindAll(arg_identifier!); + return {}; + }); + } + } + } +} + +class _TestSystemServicesHostApiCodec extends StandardMessageCodec { + const _TestSystemServicesHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is CameraPermissionsErrorData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return CameraPermissionsErrorData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestSystemServicesHostApi { + static const MessageCodec codec = _TestSystemServicesHostApiCodec(); + + Future requestCameraPermissions( + bool enableAudio); + void startListeningForDeviceOrientationChange( + bool isFrontFacing, int sensorOrientation); + void stopListeningForDeviceOrientationChange(); + static void setup(TestSystemServicesHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.requestCameraPermissions', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.requestCameraPermissions was null.'); + final List args = (message as List?)!; + final bool? arg_enableAudio = (args[0] as bool?); + assert(arg_enableAudio != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.requestCameraPermissions was null, expected non-null bool.'); + final CameraPermissionsErrorData? output = + await api.requestCameraPermissions(arg_enableAudio!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange was null.'); + final List args = (message as List?)!; + final bool? arg_isFrontFacing = (args[0] as bool?); + assert(arg_isFrontFacing != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange was null, expected non-null bool.'); + final int? arg_sensorOrientation = (args[1] as int?); + assert(arg_sensorOrientation != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange was null, expected non-null int.'); + api.startListeningForDeviceOrientationChange( + arg_isFrontFacing!, arg_sensorOrientation!); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.stopListeningForDeviceOrientationChange', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + api.stopListeningForDeviceOrientationChange(); + return {}; + }); + } + } + } +} + +class _TestPreviewHostApiCodec extends StandardMessageCodec { + const _TestPreviewHostApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is ResolutionInfo) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is ResolutionInfo) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return ResolutionInfo.decode(readValue(buffer)!); + + case 129: + return ResolutionInfo.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TestPreviewHostApi { + static const MessageCodec codec = _TestPreviewHostApiCodec(); + + void create(int identifier, int? rotation, ResolutionInfo? targetResolution); + int setSurfaceProvider(int identifier); + void releaseFlutterSurfaceTexture(); + ResolutionInfo getResolutionInfo(int identifier); + static void setup(TestPreviewHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PreviewHostApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.PreviewHostApi.create was null, expected non-null int.'); + final int? arg_rotation = (args[1] as int?); + final ResolutionInfo? arg_targetResolution = + (args[2] as ResolutionInfo?); + api.create(arg_identifier!, arg_rotation, arg_targetResolution); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.PreviewHostApi.setSurfaceProvider was null, expected non-null int.'); + final int output = api.setSurfaceProvider(arg_identifier!); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.releaseFlutterSurfaceTexture', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + api.releaseFlutterSurfaceTexture(); + return {}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.getResolutionInfo', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.PreviewHostApi.getResolutionInfo was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.PreviewHostApi.getResolutionInfo was null, expected non-null int.'); + final ResolutionInfo output = api.getResolutionInfo(arg_identifier!); + return {'result': output}; + }); + } + } + } +} diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.pigeon.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.pigeon.dart deleted file mode 100644 index 2196b73d7fdb..000000000000 --- a/packages/camera/camera_android_camerax/test/test_camerax_library.pigeon.dart +++ /dev/null @@ -1,187 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// Autogenerated from Pigeon (v3.2.9), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import -// ignore_for_file: avoid_relative_lib_imports -import 'dart:async'; -import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; -import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:camera_android_camerax/src/camerax_library.pigeon.dart'; - -class _TestJavaObjectHostApiCodec extends StandardMessageCodec { - const _TestJavaObjectHostApiCodec(); -} - -abstract class TestJavaObjectHostApi { - static const MessageCodec codec = _TestJavaObjectHostApiCodec(); - - void dispose(int identifier); - static void setup(TestJavaObjectHostApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.JavaObjectHostApi.dispose', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.JavaObjectHostApi.dispose was null.'); - final List args = (message as List?)!; - final int? arg_identifier = (args[0] as int?); - assert(arg_identifier != null, - 'Argument for dev.flutter.pigeon.JavaObjectHostApi.dispose was null, expected non-null int.'); - api.dispose(arg_identifier!); - return {}; - }); - } - } - } -} - -class _TestCameraInfoHostApiCodec extends StandardMessageCodec { - const _TestCameraInfoHostApiCodec(); -} - -abstract class TestCameraInfoHostApi { - static const MessageCodec codec = _TestCameraInfoHostApiCodec(); - - int getSensorRotationDegrees(int identifier); - static void setup(TestCameraInfoHostApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees', - codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees was null.'); - final List args = (message as List?)!; - final int? arg_identifier = (args[0] as int?); - assert(arg_identifier != null, - 'Argument for dev.flutter.pigeon.CameraInfoHostApi.getSensorRotationDegrees was null, expected non-null int.'); - final int output = api.getSensorRotationDegrees(arg_identifier!); - return {'result': output}; - }); - } - } - } -} - -class _TestCameraSelectorHostApiCodec extends StandardMessageCodec { - const _TestCameraSelectorHostApiCodec(); -} - -abstract class TestCameraSelectorHostApi { - static const MessageCodec codec = _TestCameraSelectorHostApiCodec(); - - void create(int identifier, int? lensFacing); - List filter(int identifier, List cameraInfoIds); - static void setup(TestCameraSelectorHostApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraSelectorHostApi.create', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.create was null.'); - final List args = (message as List?)!; - final int? arg_identifier = (args[0] as int?); - assert(arg_identifier != null, - 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.create was null, expected non-null int.'); - final int? arg_lensFacing = (args[1] as int?); - api.create(arg_identifier!, arg_lensFacing); - return {}; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.CameraSelectorHostApi.filter', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.filter was null.'); - final List args = (message as List?)!; - final int? arg_identifier = (args[0] as int?); - assert(arg_identifier != null, - 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.filter was null, expected non-null int.'); - final List? arg_cameraInfoIds = - (args[1] as List?)?.cast(); - assert(arg_cameraInfoIds != null, - 'Argument for dev.flutter.pigeon.CameraSelectorHostApi.filter was null, expected non-null List.'); - final List output = - api.filter(arg_identifier!, arg_cameraInfoIds!); - return {'result': output}; - }); - } - } - } -} - -class _TestProcessCameraProviderHostApiCodec extends StandardMessageCodec { - const _TestProcessCameraProviderHostApiCodec(); -} - -abstract class TestProcessCameraProviderHostApi { - static const MessageCodec codec = - _TestProcessCameraProviderHostApiCodec(); - - Future getInstance(); - List getAvailableCameraInfos(int identifier); - static void setup(TestProcessCameraProviderHostApi? api, - {BinaryMessenger? binaryMessenger}) { - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - // ignore message - final int output = await api.getInstance(); - return {'result': output}; - }); - } - } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos', - codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMockMessageHandler(null); - } else { - channel.setMockMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos was null.'); - final List args = (message as List?)!; - final int? arg_identifier = (args[0] as int?); - assert(arg_identifier != null, - 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos was null, expected non-null int.'); - final List output = - api.getAvailableCameraInfos(arg_identifier!); - return {'result': output}; - }); - } - } - } -} diff --git a/packages/camera/camera_avfoundation/CHANGELOG.md b/packages/camera/camera_avfoundation/CHANGELOG.md index d9fc8c37eb4f..f0605b7914cc 100644 --- a/packages/camera/camera_avfoundation/CHANGELOG.md +++ b/packages/camera/camera_avfoundation/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.9.11 + +* Adds back use of Optional type. +* Updates minimum Flutter version to 3.0. + +## 0.9.10+2 + +* Updates code for stricter lint checks. + ## 0.9.10+1 * Updates code for stricter lint checks. diff --git a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart index 47c1f6f0415b..524186816aab 100644 --- a/packages/camera/camera_avfoundation/example/lib/camera_controller.dart +++ b/packages/camera/camera_avfoundation/example/lib/camera_controller.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:collection'; import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/foundation.dart'; @@ -108,10 +109,10 @@ class CameraValue { bool? exposurePointSupported, bool? focusPointSupported, DeviceOrientation? deviceOrientation, - DeviceOrientation? lockedCaptureOrientation, - DeviceOrientation? recordingOrientation, + Optional? lockedCaptureOrientation, + Optional? recordingOrientation, bool? isPreviewPaused, - DeviceOrientation? previewPauseOrientation, + Optional? previewPauseOrientation, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -124,12 +125,16 @@ class CameraValue { exposureMode: exposureMode ?? this.exposureMode, focusMode: focusMode ?? this.focusMode, deviceOrientation: deviceOrientation ?? this.deviceOrientation, - lockedCaptureOrientation: - lockedCaptureOrientation ?? this.lockedCaptureOrientation, - recordingOrientation: recordingOrientation ?? this.recordingOrientation, + lockedCaptureOrientation: lockedCaptureOrientation == null + ? this.lockedCaptureOrientation + : lockedCaptureOrientation.orNull, + recordingOrientation: recordingOrientation == null + ? this.recordingOrientation + : recordingOrientation.orNull, isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, - previewPauseOrientation: - previewPauseOrientation ?? this.previewPauseOrientation, + previewPauseOrientation: previewPauseOrientation == null + ? this.previewPauseOrientation + : previewPauseOrientation.orNull, ); } @@ -257,14 +262,16 @@ class CameraController extends ValueNotifier { await CameraPlatform.instance.pausePreview(_cameraId); value = value.copyWith( isPreviewPaused: true, - previewPauseOrientation: - value.lockedCaptureOrientation ?? value.deviceOrientation); + previewPauseOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); } /// Resumes the current camera preview Future resumePreview() async { await CameraPlatform.instance.resumePreview(_cameraId); - value = value.copyWith(isPreviewPaused: false); + value = value.copyWith( + isPreviewPaused: false, + previewPauseOrientation: const Optional.absent()); } /// Captures an image and returns the file where it was saved. @@ -307,8 +314,8 @@ class CameraController extends ValueNotifier { isRecordingVideo: true, isRecordingPaused: false, isStreamingImages: streamCallback != null, - recordingOrientation: - value.lockedCaptureOrientation ?? value.deviceOrientation); + recordingOrientation: Optional.of( + value.lockedCaptureOrientation ?? value.deviceOrientation)); } /// Stops the video recording and returns the file where it was saved. @@ -321,7 +328,10 @@ class CameraController extends ValueNotifier { final XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); - value = value.copyWith(isRecordingVideo: false); + value = value.copyWith( + isRecordingVideo: false, + recordingOrientation: const Optional.absent(), + ); return file; } @@ -389,12 +399,16 @@ class CameraController extends ValueNotifier { Future lockCaptureOrientation() async { await CameraPlatform.instance .lockCaptureOrientation(_cameraId, value.deviceOrientation); - value = value.copyWith(lockedCaptureOrientation: value.deviceOrientation); + value = value.copyWith( + lockedCaptureOrientation: + Optional.of(value.deviceOrientation)); } /// Unlocks the capture orientation. Future unlockCaptureOrientation() async { await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); + value = value.copyWith( + lockedCaptureOrientation: const Optional.absent()); } /// Sets the focus mode for taking pictures. @@ -428,3 +442,112 @@ class CameraController extends ValueNotifier { } } } + +/// A value that might be absent. +/// +/// Used to represent [DeviceOrientation]s that are optional but also able +/// to be cleared. +@immutable +class Optional extends IterableBase { + /// Constructs an empty Optional. + const Optional.absent() : _value = null; + + /// Constructs an Optional of the given [value]. + /// + /// Throws [ArgumentError] if [value] is null. + Optional.of(T value) : _value = value { + // TODO(cbracken): Delete and make this ctor const once mixed-mode + // execution is no longer around. + ArgumentError.checkNotNull(value); + } + + /// Constructs an Optional of the given [value]. + /// + /// If [value] is null, returns [absent()]. + const Optional.fromNullable(T? value) : _value = value; + + final T? _value; + + /// True when this optional contains a value. + bool get isPresent => _value != null; + + /// True when this optional contains no value. + bool get isNotPresent => _value == null; + + /// Gets the Optional value. + /// + /// Throws [StateError] if [value] is null. + T get value { + if (_value == null) { + throw StateError('value called on absent Optional.'); + } + return _value!; + } + + /// Executes a function if the Optional value is present. + void ifPresent(void Function(T value) ifPresent) { + if (isPresent) { + ifPresent(_value as T); + } + } + + /// Execution a function if the Optional value is absent. + void ifAbsent(void Function() ifAbsent) { + if (!isPresent) { + ifAbsent(); + } + } + + /// Gets the Optional value with a default. + /// + /// The default is returned if the Optional is [absent()]. + /// + /// Throws [ArgumentError] if [defaultValue] is null. + T or(T defaultValue) { + return _value ?? defaultValue; + } + + /// Gets the Optional value, or `null` if there is none. + T? get orNull => _value; + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// The transformer must not return `null`. If it does, an [ArgumentError] is thrown. + Optional transform(S Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.of(transformer(_value as T)); + } + + /// Transforms the Optional value. + /// + /// If the Optional is [absent()], returns [absent()] without applying the transformer. + /// + /// Returns [absent()] if the transformer returns `null`. + Optional transformNullable(S? Function(T value) transformer) { + return _value == null + ? Optional.absent() + : Optional.fromNullable(transformer(_value as T)); + } + + @override + Iterator get iterator => + isPresent ? [_value as T].iterator : Iterable.empty().iterator; + + /// Delegates to the underlying [value] hashCode. + @override + int get hashCode => _value.hashCode; + + /// Delegates to the underlying [value] operator==. + @override + bool operator ==(Object o) => o is Optional && o._value == _value; + + @override + String toString() { + return _value == null + ? 'Optional { absent }' + : 'Optional { value: $_value }'; + } +} diff --git a/packages/camera/camera_avfoundation/example/lib/main.dart b/packages/camera/camera_avfoundation/example/lib/main.dart index af9aab1a8a86..4d98aed9a4c2 100644 --- a/packages/camera/camera_avfoundation/example/lib/main.dart +++ b/packages/camera/camera_avfoundation/example/lib/main.dart @@ -35,9 +35,11 @@ IconData getCameraLensIcon(CameraLensDirection direction) { return Icons.camera_front; case CameraLensDirection.external: return Icons.camera; - default: - throw ArgumentError('Unknown lens direction'); } + // This enum is from a different package, so a new value could be added at + // any time. The example should keep working if that happens. + // ignore: dead_code + return Icons.camera; } void _logError(String code, String? message) { @@ -1089,5 +1091,4 @@ Future main() async { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/camera/camera_avfoundation/example/pubspec.yaml b/packages/camera/camera_avfoundation/example/pubspec.yaml index a9252cbd6d61..7c85ba807193 100644 --- a/packages/camera/camera_avfoundation/example/pubspec.yaml +++ b/packages/camera/camera_avfoundation/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: camera_avfoundation: diff --git a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart index 11bda5155819..5080c57a736f 100644 --- a/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart +++ b/packages/camera/camera_avfoundation/lib/src/avfoundation_camera.dart @@ -145,6 +145,7 @@ class AVFoundationCamera extends CameraPlatform { // ignore: body_might_complete_normally_catch_error (Object error, StackTrace stackTrace) { if (error is! PlatformException) { + // ignore: only_throw_errors throw error; } completer.completeError( @@ -526,9 +527,14 @@ class AVFoundationCamera extends CameraPlatform { return 'always'; case FlashMode.torch: return 'torch'; - default: - throw ArgumentError('Unknown FlashMode value'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return 'off'; } /// Returns the resolution preset as a String. @@ -546,9 +552,14 @@ class AVFoundationCamera extends CameraPlatform { return 'medium'; case ResolutionPreset.low: return 'low'; - default: - throw ArgumentError('Unknown ResolutionPreset value'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return 'max'; } /// Converts messages received from the native platform into device events. diff --git a/packages/camera/camera_avfoundation/lib/src/utils.dart b/packages/camera/camera_avfoundation/lib/src/utils.dart index 663ec6da7a97..8d58f7fe1297 100644 --- a/packages/camera/camera_avfoundation/lib/src/utils.dart +++ b/packages/camera/camera_avfoundation/lib/src/utils.dart @@ -29,9 +29,14 @@ String serializeDeviceOrientation(DeviceOrientation orientation) { return 'landscapeRight'; case DeviceOrientation.landscapeLeft: return 'landscapeLeft'; - default: - throw ArgumentError('Unknown DeviceOrientation value'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return 'portraitUp'; } /// Returns the device orientation for a given String. diff --git a/packages/camera/camera_avfoundation/pubspec.yaml b/packages/camera/camera_avfoundation/pubspec.yaml index 975accff84b9..b272a4c5c68d 100644 --- a/packages/camera/camera_avfoundation/pubspec.yaml +++ b/packages/camera/camera_avfoundation/pubspec.yaml @@ -2,11 +2,11 @@ name: camera_avfoundation description: iOS implementation of the camera plugin. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_avfoundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.9.10+1 +version: 0.9.11 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart index 50d3e9875be1..5d0b74cf0c0c 100644 --- a/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart +++ b/packages/camera/camera_avfoundation/test/avfoundation_camera_test.dart @@ -32,14 +32,15 @@ void main() { // registerWith is called very early in initialization the bindings won't // have been initialized. While registerWith could intialize them, that // could slow down startup, so instead the handler should be set up lazily. - final ByteData? response = await TestDefaultBinaryMessengerBinding - .instance!.defaultBinaryMessenger - .handlePlatformMessage( - AVFoundationCamera.deviceEventChannelName, - const StandardMethodCodec().encodeMethodCall(const MethodCall( - 'orientation_changed', - {'orientation': 'portraitDown'})), - (ByteData? data) {}); + final ByteData? response = + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + AVFoundationCamera.deviceEventChannelName, + const StandardMethodCodec().encodeMethodCall(const MethodCall( + 'orientation_changed', + {'orientation': 'portraitDown'})), + (ByteData? data) {}); expect(response, null); }); @@ -421,7 +422,8 @@ void main() { const DeviceOrientationChangedEvent event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); for (int i = 0; i < 3; i++) { - await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( AVFoundationCamera.deviceEventChannelName, const StandardMethodCodec().encodeMethodCall( @@ -1122,3 +1124,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/camera/camera_avfoundation/test/method_channel_mock.dart b/packages/camera/camera_avfoundation/test/method_channel_mock.dart index 413c10633cc1..f26d12a3688a 100644 --- a/packages/camera/camera_avfoundation/test/method_channel_mock.dart +++ b/packages/camera/camera_avfoundation/test/method_channel_mock.dart @@ -11,7 +11,9 @@ class MethodChannelMock { this.delay, required this.methods, }) : methodChannel = MethodChannel(channelName) { - methodChannel.setMockMethodCallHandler(_handler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, _handler); } final Duration? delay; @@ -37,3 +39,9 @@ class MethodChannelMock { }); } } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md index 55d93a3057bd..b51eb9c78a43 100644 --- a/packages/camera/camera_platform_interface/CHANGELOG.md +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.4.0 + +* Allows camera to be switched while video recording. +* Updates minimum Flutter version to 3.0. + +## 2.3.4 + +* Updates code for stricter lint checks. + ## 2.3.3 * Updates code for stricter lint checks. 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 index 06c1ac69570f..14d20fc817b2 100644 --- 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 @@ -137,6 +137,7 @@ class MethodChannelCamera extends CameraPlatform { // ignore: body_might_complete_normally_catch_error (Object error, StackTrace stackTrace) { if (error is! PlatformException) { + // ignore: only_throw_errors throw error; } completer.completeError( @@ -503,6 +504,17 @@ class MethodChannelCamera extends CameraPlatform { ); } + @override + Future setDescriptionWhileRecording( + CameraDescription description) async { + await _channel.invokeMethod( + 'setDescriptionWhileRecording', + { + 'cameraName': description.name, + }, + ); + } + @override Widget buildPreview(int cameraId) { return Texture(textureId: cameraId); @@ -519,8 +531,6 @@ class MethodChannelCamera extends CameraPlatform { return 'always'; case FlashMode.torch: return 'torch'; - default: - throw ArgumentError('Unknown FlashMode value'); } } @@ -539,8 +549,6 @@ class MethodChannelCamera extends CameraPlatform { return 'medium'; case ResolutionPreset.low: return 'low'; - default: - throw ArgumentError('Unknown ResolutionPreset value'); } } 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 index b3e5b8f82afa..b43629d4e0c3 100644 --- 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 @@ -269,6 +269,12 @@ abstract class CameraPlatform extends PlatformInterface { throw UnimplementedError('pausePreview() is not implemented.'); } + /// Sets the active camera while recording. + Future setDescriptionWhileRecording(CameraDescription description) { + throw UnimplementedError( + 'setDescriptionWhileRecording() is not implemented.'); + } + /// Returns a widget showing a live camera preview. Widget buildPreview(int cameraId) { throw UnimplementedError('buildView() has not been implemented.'); 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 index 56a05cd2d0f1..6da44c98ddc8 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart @@ -18,8 +18,6 @@ String serializeExposureMode(ExposureMode exposureMode) { return 'locked'; case ExposureMode.auto: return 'auto'; - default: - throw ArgumentError('Unknown ExposureMode value'); } } 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 index 6baae0c1f63e..1f9cbef1bab9 100644 --- a/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart +++ b/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart @@ -18,8 +18,6 @@ String serializeFocusMode(FocusMode focusMode) { return 'locked'; case FocusMode.auto: return 'auto'; - default: - throw ArgumentError('Unknown 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 index edbf7d24098c..8dc69e09f58a 100644 --- 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 @@ -47,7 +47,6 @@ extension ImageFormatGroupName on ImageFormatGroup { case ImageFormatGroup.jpeg: return 'jpeg'; case ImageFormatGroup.unknown: - default: return 'unknown'; } } diff --git a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart index d86880afd216..771a94be416e 100644 --- a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart +++ b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart @@ -30,8 +30,6 @@ String serializeDeviceOrientation(DeviceOrientation orientation) { return 'landscapeRight'; case DeviceOrientation.landscapeLeft: return 'landscapeLeft'; - default: - throw ArgumentError('Unknown DeviceOrientation value'); } } diff --git a/packages/camera/camera_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml index 54383cd853ee..4cdb2855a156 100644 --- a/packages/camera/camera_platform_interface/pubspec.yaml +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -4,11 +4,11 @@ repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.3.3 +version: 2.4.0 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: cross_file: ^0.3.1 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 index ed6151522f0c..b01123d7cb29 100644 --- 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 @@ -576,6 +576,29 @@ void main() { ]); }); + test('Should set description while recording', () async { + // Arrange + final MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setDescriptionWhileRecording': null}, + ); + + // Act + const CameraDescription cameraDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0); + await camera.setDescriptionWhileRecording(cameraDescription); + + // Assert + expect(channel.log, [ + isMethodCall('setDescriptionWhileRecording', + arguments: { + 'cameraName': cameraDescription.name + }), + ]); + }); + test('Should pass maxVideoDuration when starting recording a video', () async { // Arrange 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 index 413c10633cc1..f26d12a3688a 100644 --- a/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart +++ b/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart @@ -11,7 +11,9 @@ class MethodChannelMock { this.delay, required this.methods, }) : methodChannel = MethodChannel(channelName) { - methodChannel.setMockMethodCallHandler(_handler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, _handler); } final Duration? delay; @@ -37,3 +39,9 @@ class MethodChannelMock { }); } } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md index d8d0c93dde11..2a8d43b95e18 100644 --- a/packages/camera/camera_web/CHANGELOG.md +++ b/packages/camera/camera_web/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 0.3.1+1 + +* Updates code for stricter lint checks. + ## 0.3.1 * Updates to latest camera platform interface, and fails if user attempts to use streaming with recording (since streaming is currently unsupported on web). diff --git a/packages/camera/camera_web/example/pubspec.yaml b/packages/camera/camera_web/example/pubspec.yaml index e82bbe392ceb..ee66870c051d 100644 --- a/packages/camera/camera_web/example/pubspec.yaml +++ b/packages/camera/camera_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/camera/camera_web/lib/src/camera_service.dart b/packages/camera/camera_web/lib/src/camera_service.dart index 6e20c7d74f78..451278c23fc3 100644 --- a/packages/camera/camera_web/lib/src/camera_service.dart +++ b/packages/camera/camera_web/lib/src/camera_service.dart @@ -299,9 +299,15 @@ class CameraService { case ResolutionPreset.medium: return const Size(720, 480); case ResolutionPreset.low: - default: return const Size(320, 240); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return const Size(320, 240); } /// Maps the given [deviceOrientation] to [OrientationType]. diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml index 2368e62abee6..101444b98fe4 100644 --- a/packages/camera/camera_web/pubspec.yaml +++ b/packages/camera/camera_web/pubspec.yaml @@ -2,11 +2,11 @@ name: camera_web description: A Flutter plugin for getting information about and controlling the camera on Web. repository: https://github.com/flutter/plugins/tree/main/packages/camera/camera_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.3.1 +version: 0.3.1+1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/camera/camera_windows/CHANGELOG.md b/packages/camera/camera_windows/CHANGELOG.md index a6269b955983..34ee66815aa6 100644 --- a/packages/camera/camera_windows/CHANGELOG.md +++ b/packages/camera/camera_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 0.2.1+4 * Updates code for stricter lint checks. diff --git a/packages/camera/camera_windows/example/pubspec.yaml b/packages/camera/camera_windows/example/pubspec.yaml index 80ce958a0e84..69ce1c330156 100644 --- a/packages/camera/camera_windows/example/pubspec.yaml +++ b/packages/camera/camera_windows/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: 'none' environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: camera_platform_interface: ^2.1.2 diff --git a/packages/camera/camera_windows/pubspec.yaml b/packages/camera/camera_windows/pubspec.yaml index d87491a6c0cf..e028559c28ab 100644 --- a/packages/camera/camera_windows/pubspec.yaml +++ b/packages/camera/camera_windows/pubspec.yaml @@ -6,7 +6,7 @@ version: 0.2.1+4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/camera/camera_windows/test/utils/method_channel_mock.dart b/packages/camera/camera_windows/test/utils/method_channel_mock.dart index 22f7ecead589..559f60662844 100644 --- a/packages/camera/camera_windows/test/utils/method_channel_mock.dart +++ b/packages/camera/camera_windows/test/utils/method_channel_mock.dart @@ -17,7 +17,9 @@ class MethodChannelMock { this.delay, required this.methods, }) : methodChannel = MethodChannel(channelName) { - methodChannel.setMockMethodCallHandler(_handler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(methodChannel, _handler); } final Duration? delay; @@ -43,3 +45,9 @@ class MethodChannelMock { }); } } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index d2652dc64ef7..96ccb32f0325 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.2.0+8 + +* Updates espresso and junit dependencies. + +## 0.2.0+7 + +* Updates espresso gradle and gson dependencies. +* Updates minimum Flutter version to 3.0. + ## 0.2.0+6 * Updates espresso-accessibility to 3.5.1. diff --git a/packages/espresso/android/build.gradle b/packages/espresso/android/build.gradle index 1444eb9f8a85..bda13fc52780 100644 --- a/packages/espresso/android/build.gradle +++ b/packages/espresso/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:7.4.1' } } @@ -29,10 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' baseline file("lint-baseline.xml") } @@ -53,7 +50,7 @@ android { dependencies { implementation 'com.google.guava:guava:31.1-android' implementation 'com.squareup.okhttp3:okhttp:4.10.0' - implementation 'com.google.code.gson:gson:2.9.1' + implementation 'com.google.code.gson:gson:2.10.1' androidTestImplementation 'org.hamcrest:hamcrest:2.2' testImplementation 'junit:junit:4.13.2' @@ -69,17 +66,17 @@ dependencies { api 'androidx.test:rules:1.1.0' // Assertions - api 'androidx.test.ext:junit:1.1.3' + api 'androidx.test.ext:junit:1.1.5' api 'androidx.test.ext:truth:1.5.0' api 'com.google.truth:truth:0.42' // Espresso dependencies - api 'androidx.test.espresso:espresso-core:3.1.0' - api 'androidx.test.espresso:espresso-contrib:3.1.0' - api 'androidx.test.espresso:espresso-intents:3.1.0' + api 'androidx.test.espresso:espresso-core:3.5.1' + api 'androidx.test.espresso:espresso-contrib:3.5.1' + api 'androidx.test.espresso:espresso-intents:3.5.1' api 'androidx.test.espresso:espresso-accessibility:3.5.1' - api 'androidx.test.espresso:espresso-web:3.1.0' - api 'androidx.test.espresso.idling:idling-concurrent:3.1.0' + api 'androidx.test.espresso:espresso-web:3.5.1' + api 'androidx.test.espresso.idling:idling-concurrent:3.5.1' // The following Espresso dependency can be either "implementation" // or "androidTestImplementation", depending on whether you want the diff --git a/packages/espresso/example/android/gradle.properties b/packages/espresso/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/espresso/example/android/gradle.properties +++ b/packages/espresso/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/espresso/example/pubspec.yaml b/packages/espresso/example/pubspec.yaml index 67f9edcd4644..0adf623b728a 100644 --- a/packages/espresso/example/pubspec.yaml +++ b/packages/espresso/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml index 984dd70e4318..21aa5dfb27d9 100644 --- a/packages/espresso/pubspec.yaml +++ b/packages/espresso/pubspec.yaml @@ -3,11 +3,11 @@ description: Java classes for testing Flutter apps using Espresso. Allows driving Flutter widgets from a native Espresso test. repository: https://github.com/flutter/plugins/tree/main/packages/espresso issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+espresso%22 -version: 0.2.0+6 +version: 0.2.0+8 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md index 7983aa57561f..9fd2341501b3 100644 --- a/packages/file_selector/file_selector/CHANGELOG.md +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + ## 0.9.2+2 * Improves API docs and examples. diff --git a/packages/file_selector/file_selector/example/lib/get_directory_page.dart b/packages/file_selector/file_selector/example/lib/get_directory_page.dart index de80aa56be56..dfe166db96c4 100644 --- a/packages/file_selector/file_selector/example/lib/get_directory_page.dart +++ b/packages/file_selector/file_selector/example/lib/get_directory_page.dart @@ -22,10 +22,12 @@ class GetDirectoryPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(directoryPath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(directoryPath), + ); + } } @override diff --git a/packages/file_selector/file_selector/example/lib/open_image_page.dart b/packages/file_selector/file_selector/example/lib/open_image_page.dart index ba18e6e78594..7717f28c39fe 100644 --- a/packages/file_selector/file_selector/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector/example/lib/open_image_page.dart @@ -29,10 +29,12 @@ class OpenImagePage extends StatelessWidget { final String fileName = file.name; final String filePath = file.path; - await showDialog( - context: context, - builder: (BuildContext context) => ImageDisplay(fileName, filePath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } } @override diff --git a/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart index 8ae83c2a85dc..a09a6db9d7a7 100644 --- a/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart @@ -32,10 +32,12 @@ class OpenMultipleImagesPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => MultipleImagesDisplay(files), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } } @override diff --git a/packages/file_selector/file_selector/example/lib/open_text_page.dart b/packages/file_selector/file_selector/example/lib/open_text_page.dart index f052db1eefc1..e28a67a02ddf 100644 --- a/packages/file_selector/file_selector/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector/example/lib/open_text_page.dart @@ -32,10 +32,12 @@ class OpenTextPage extends StatelessWidget { final String fileName = file.name; final String fileContent = await file.readAsString(); - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(fileName, fileContent), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } } @override diff --git a/packages/file_selector/file_selector/example/pubspec.yaml b/packages/file_selector/file_selector/example/pubspec.yaml index 011d95874ae4..ff9d6d0d2e17 100644 --- a/packages/file_selector/file_selector/example/pubspec.yaml +++ b/packages/file_selector/file_selector/example/pubspec.yaml @@ -6,6 +6,7 @@ version: 1.0.0+1 environment: sdk: ">=2.12.0 <3.0.0" + flutter: ">=3.0.0" dependencies: file_selector: diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml index ad187d6f446a..17e41cd656dd 100644 --- a/packages/file_selector/file_selector/pubspec.yaml +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -7,7 +7,7 @@ version: 0.9.2+2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/file_selector/file_selector_ios/CHANGELOG.md b/packages/file_selector/file_selector_ios/CHANGELOG.md index 439e1d4fd4c1..40d232ed25d0 100644 --- a/packages/file_selector/file_selector_ios/CHANGELOG.md +++ b/packages/file_selector/file_selector_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + ## 0.5.0+2 * Changes XTypeGroup initialization from final to const. diff --git a/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart b/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart index 606a64870566..6fcbcbfbafd6 100644 --- a/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector_ios/example/lib/open_image_page.dart @@ -29,10 +29,12 @@ class OpenImagePage extends StatelessWidget { final String fileName = file.name; final String filePath = file.path; - await showDialog( - context: context, - builder: (BuildContext context) => ImageDisplay(fileName, filePath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } } @override diff --git a/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart index adc4a65f12b5..30cc5159b060 100644 --- a/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector_ios/example/lib/open_multiple_images_page.dart @@ -34,10 +34,12 @@ class OpenMultipleImagesPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => MultipleImagesDisplay(files), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } } @override diff --git a/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart b/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart index e7bbf8bc937f..f21daf9a96bf 100644 --- a/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector_ios/example/lib/open_text_page.dart @@ -26,10 +26,12 @@ class OpenTextPage extends StatelessWidget { final String fileName = file.name; final String fileContent = await file.readAsString(); - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(fileName, fileContent), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } } @override diff --git a/packages/file_selector/file_selector_ios/example/pubspec.yaml b/packages/file_selector/file_selector_ios/example/pubspec.yaml index 5a2eaa6f7dcd..175ec6c6e7d0 100644 --- a/packages/file_selector/file_selector_ios/example/pubspec.yaml +++ b/packages/file_selector/file_selector_ios/example/pubspec.yaml @@ -5,6 +5,7 @@ version: 1.0.0 environment: sdk: ">=2.14.4 <3.0.0" + flutter: ">=3.0.0" dependencies: # The following adds the Cupertino Icons font to your application. @@ -28,4 +29,4 @@ dev_dependencies: sdk: flutter flutter: - uses-material-design: true \ No newline at end of file + uses-material-design: true diff --git a/packages/file_selector/file_selector_ios/pigeons/messages.dart b/packages/file_selector/file_selector_ios/pigeons/messages.dart index d0ea73cde111..66706cc2406e 100644 --- a/packages/file_selector/file_selector_ios/pigeons/messages.dart +++ b/packages/file_selector/file_selector_ios/pigeons/messages.dart @@ -6,7 +6,7 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', - dartTestOut: 'test/test_api.dart', + dartTestOut: 'test/test_api.g.dart', objcHeaderOut: 'ios/Classes/messages.g.h', objcSourceOut: 'ios/Classes/messages.g.m', objcOptions: ObjcOptions( diff --git a/packages/file_selector/file_selector_ios/pubspec.yaml b/packages/file_selector/file_selector_ios/pubspec.yaml index 3f8ecfac04ce..e772cb7d8632 100644 --- a/packages/file_selector/file_selector_ios/pubspec.yaml +++ b/packages/file_selector/file_selector_ios/pubspec.yaml @@ -6,7 +6,7 @@ version: 0.5.0+2 environment: sdk: ">=2.14.4 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart index f66bd7dc7ced..e10ad17a2fb4 100644 --- a/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart +++ b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart @@ -11,7 +11,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'file_selector_ios_test.mocks.dart'; -import 'test_api.dart'; +import 'test_api.g.dart'; @GenerateMocks([TestFileSelectorApi]) void main() { diff --git a/packages/file_selector/file_selector_ios/test/file_selector_ios_test.mocks.dart b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.mocks.dart index 38c91b46f65e..1d22ba75a10a 100644 --- a/packages/file_selector/file_selector_ios/test/file_selector_ios_test.mocks.dart +++ b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.mocks.dart @@ -8,7 +8,7 @@ import 'dart:async' as _i3; import 'package:file_selector_ios/src/messages.g.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'test_api.dart' as _i2; +import 'test_api.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values diff --git a/packages/file_selector/file_selector_ios/test/test_api.dart b/packages/file_selector/file_selector_ios/test/test_api.g.dart similarity index 100% rename from packages/file_selector/file_selector_ios/test/test_api.dart rename to packages/file_selector/file_selector_ios/test/test_api.g.dart diff --git a/packages/file_selector/file_selector_linux/CHANGELOG.md b/packages/file_selector/file_selector_linux/CHANGELOG.md index a1f57b5cc857..6f7853cc5f13 100644 --- a/packages/file_selector/file_selector_linux/CHANGELOG.md +++ b/packages/file_selector/file_selector_linux/CHANGELOG.md @@ -1,3 +1,12 @@ +## NEXT + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + +## 0.9.1 + +* Adds `getDirectoryPaths` implementation. + ## 0.9.0+1 * Changes XTypeGroup initialization from final to const. diff --git a/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart b/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart index 0699dd121541..f6390ccef20d 100644 --- a/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart +++ b/packages/file_selector/file_selector_linux/example/lib/get_directory_page.dart @@ -21,10 +21,12 @@ class GetDirectoryPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(directoryPath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(directoryPath), + ); + } } @override diff --git a/packages/file_selector/file_selector_linux/example/lib/get_multiple_directories_page.dart b/packages/file_selector/file_selector_linux/example/lib/get_multiple_directories_page.dart new file mode 100644 index 000000000000..087240be765e --- /dev/null +++ b/packages/file_selector/file_selector_linux/example/lib/get_multiple_directories_page.dart @@ -0,0 +1,87 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/material.dart'; + +/// Screen that allows the user to select one or more directories using `getDirectoryPaths`, +/// then displays the selected directories in a dialog. +class GetMultipleDirectoriesPage extends StatelessWidget { + /// Default Constructor + const GetMultipleDirectoriesPage({Key? key}) : super(key: key); + + Future _getDirectoryPaths(BuildContext context) async { + const String confirmButtonText = 'Choose'; + final List directoryPaths = + await FileSelectorPlatform.instance.getDirectoryPaths( + confirmButtonText: confirmButtonText, + ); + if (directoryPaths.isEmpty) { + // Operation was canceled by the user. + return; + } + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => + TextDisplay(directoryPaths.join('\n')), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Select multiple directories'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + // TODO(darrenaustin): Migrate to new API once it lands in stable: https://github.com/flutter/flutter/issues/105724 + // ignore: deprecated_member_use + primary: Colors.blue, + // ignore: deprecated_member_use + onPrimary: Colors.white, + ), + child: const Text( + 'Press to ask user to choose multiple directories'), + onPressed: () => _getDirectoryPaths(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog. +class TextDisplay extends StatelessWidget { + /// Creates a `TextDisplay`. + const TextDisplay(this.directoriesPaths, {Key? key}) : super(key: key); + + /// The path selected in the dialog. + final String directoriesPaths; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Selected Directories'), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(directoriesPaths), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector_linux/example/lib/home_page.dart b/packages/file_selector/file_selector_linux/example/lib/home_page.dart index a4b2ae1f63ea..80e16332a017 100644 --- a/packages/file_selector/file_selector_linux/example/lib/home_page.dart +++ b/packages/file_selector/file_selector_linux/example/lib/home_page.dart @@ -55,6 +55,13 @@ class HomePage extends StatelessWidget { child: const Text('Open a get directory dialog'), onPressed: () => Navigator.pushNamed(context, '/directory'), ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open a get directories dialog'), + onPressed: () => + Navigator.pushNamed(context, '/multi-directories'), + ), ], ), ), diff --git a/packages/file_selector/file_selector_linux/example/lib/main.dart b/packages/file_selector/file_selector_linux/example/lib/main.dart index 3e447104ef9f..b8f047645a1d 100644 --- a/packages/file_selector/file_selector_linux/example/lib/main.dart +++ b/packages/file_selector/file_selector_linux/example/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'get_directory_page.dart'; +import 'get_multiple_directories_page.dart'; import 'home_page.dart'; import 'open_image_page.dart'; import 'open_multiple_images_page.dart'; @@ -36,6 +37,8 @@ class MyApp extends StatelessWidget { '/open/text': (BuildContext context) => const OpenTextPage(), '/save/text': (BuildContext context) => SaveTextPage(), '/directory': (BuildContext context) => const GetDirectoryPage(), + '/multi-directories': (BuildContext context) => + const GetMultipleDirectoriesPage() }, ); } diff --git a/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart index b6ada56ebb2b..9252d25f113c 100644 --- a/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector_linux/example/lib/open_image_page.dart @@ -28,10 +28,12 @@ class OpenImagePage extends StatelessWidget { final String fileName = file.name; final String filePath = file.path; - await showDialog( - context: context, - builder: (BuildContext context) => ImageDisplay(fileName, filePath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } } @override diff --git a/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart index c8e352a5b8bd..787717cdea13 100644 --- a/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector_linux/example/lib/open_multiple_images_page.dart @@ -32,10 +32,12 @@ class OpenMultipleImagesPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => MultipleImagesDisplay(files), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } } @override diff --git a/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart b/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart index 4c88d7475049..97812f2b3505 100644 --- a/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector_linux/example/lib/open_text_page.dart @@ -25,10 +25,12 @@ class OpenTextPage extends StatelessWidget { final String fileName = file.name; final String fileContent = await file.readAsString(); - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(fileName, fileContent), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } } @override diff --git a/packages/file_selector/file_selector_linux/example/pubspec.yaml b/packages/file_selector/file_selector_linux/example/pubspec.yaml index 51bdb28717aa..f90d1c88ef97 100644 --- a/packages/file_selector/file_selector_linux/example/pubspec.yaml +++ b/packages/file_selector/file_selector_linux/example/pubspec.yaml @@ -1,15 +1,16 @@ name: file_selector_linux_example description: Local testbed for Linux file_selector implementation. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: 'none' version: 1.0.0+1 environment: sdk: ">=2.12.0 <3.0.0" + flutter: ">=3.0.0" dependencies: file_selector_linux: path: ../ - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.4.0 flutter: sdk: flutter diff --git a/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart index 430b41c398db..b8e3df6a11bd 100644 --- a/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart +++ b/packages/file_selector/file_selector_linux/lib/file_selector_linux.dart @@ -102,13 +102,26 @@ class FileSelectorLinux extends FileSelectorPlatform { String? initialDirectory, String? confirmButtonText, }) async { - return _channel.invokeMethod( - _getDirectoryPathMethod, - { - _initialDirectoryKey: initialDirectory, - _confirmButtonTextKey: confirmButtonText, - }, - ); + final List? path = await _channel + .invokeListMethod(_getDirectoryPathMethod, { + _initialDirectoryKey: initialDirectory, + _confirmButtonTextKey: confirmButtonText, + }); + return path?.first; + } + + @override + Future> getDirectoryPaths({ + String? initialDirectory, + String? confirmButtonText, + }) async { + final List? pathList = await _channel + .invokeListMethod(_getDirectoryPathMethod, { + _initialDirectoryKey: initialDirectory, + _confirmButtonTextKey: confirmButtonText, + _multipleKey: true, + }); + return pathList ?? []; } } diff --git a/packages/file_selector/file_selector_linux/linux/.gitignore b/packages/file_selector/file_selector_linux/linux/.gitignore new file mode 100644 index 000000000000..83fee186aa98 --- /dev/null +++ b/packages/file_selector/file_selector_linux/linux/.gitignore @@ -0,0 +1,2 @@ +CMakeCache.txt +CMakeFiles/ \ No newline at end of file diff --git a/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc b/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc index 833771955120..5a8cc2132595 100644 --- a/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc +++ b/packages/file_selector/file_selector_linux/linux/file_selector_plugin.cc @@ -192,10 +192,10 @@ static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, FlValue* args = fl_method_call_get_args(method_call); g_autoptr(FlMethodResponse) response = nullptr; - if (strcmp(method, kOpenFileMethod) == 0) { + if (strcmp(method, kOpenFileMethod) == 0 || + strcmp(method, kGetDirectoryPathMethod) == 0) { response = show_dialog(self, method, args, true); - } else if (strcmp(method, kGetDirectoryPathMethod) == 0 || - strcmp(method, kGetSavePathMethod) == 0) { + } else if (strcmp(method, kGetSavePathMethod) == 0) { response = show_dialog(self, method, args, false); } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); diff --git a/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc b/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc index 84c55ac91900..8762b4a5f9f6 100644 --- a/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc +++ b/packages/file_selector/file_selector_linux/linux/test/file_selector_plugin_test.cc @@ -169,3 +169,17 @@ TEST(FileSelectorPlugin, TestGetDirectory) { EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)), false); } + +TEST(FileSelectorPlugin, TestGetMultipleDirectories) { + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string_take(args, "multiple", fl_value_new_bool(true)); + + g_autoptr(GtkFileChooserNative) dialog = + create_dialog_for_method(nullptr, "getDirectoryPath", args); + + ASSERT_NE(dialog, nullptr); + EXPECT_EQ(gtk_file_chooser_get_action(GTK_FILE_CHOOSER(dialog)), + GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER); + EXPECT_EQ(gtk_file_chooser_get_select_multiple(GTK_FILE_CHOOSER(dialog)), + true); +} diff --git a/packages/file_selector/file_selector_linux/pubspec.yaml b/packages/file_selector/file_selector_linux/pubspec.yaml index a8aea37d72e2..af88485b0ef2 100644 --- a/packages/file_selector/file_selector_linux/pubspec.yaml +++ b/packages/file_selector/file_selector_linux/pubspec.yaml @@ -2,11 +2,11 @@ name: file_selector_linux description: Liunx implementation of the file_selector plugin. repository: https://github.com/flutter/plugins/tree/main/packages/file_selector/file_selector_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.9.0+1 +version: 0.9.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -18,7 +18,7 @@ flutter: dependencies: cross_file: ^0.3.1 - file_selector_platform_interface: ^2.2.0 + file_selector_platform_interface: ^2.4.0 flutter: sdk: flutter diff --git a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart index 748f922ae6ef..53a549da3d4a 100644 --- a/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart +++ b/packages/file_selector/file_selector_linux/test/file_selector_linux_test.dart @@ -16,10 +16,15 @@ void main() { setUp(() { plugin = FileSelectorLinux(); log = []; - plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return null; - }); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + plugin.channel, + (MethodCall methodCall) async { + log.add(methodCall); + return null; + }, + ); }); test('registers instance', () { @@ -46,57 +51,54 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group, groupTwo]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'text', - 'extensions': ['*.txt'], - 'mimeTypes': ['text/plain'], - }, - { - 'label': 'image', - 'extensions': ['*.jpg'], - 'mimeTypes': ['image/jpg'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'text', + 'extensions': ['*.txt'], + 'mimeTypes': ['text/plain'], + }, + { + 'label': 'image', + 'extensions': ['*.jpg'], + 'mimeTypes': ['image/jpg'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }, ); }); test('passes initialDirectory correctly', () async { await plugin.openFile(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': false, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.openFile(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': false, + }, ); }); @@ -118,21 +120,20 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'any', - 'extensions': ['*'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'any', + 'extensions': ['*'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }, ); }); }); @@ -156,57 +157,54 @@ void main() { await plugin.openFiles(acceptedTypeGroups: [group, groupTwo]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'text', - 'extensions': ['*.txt'], - 'mimeTypes': ['text/plain'], - }, - { - 'label': 'image', - 'extensions': ['*.jpg'], - 'mimeTypes': ['image/jpg'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': true, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'text', + 'extensions': ['*.txt'], + 'mimeTypes': ['text/plain'], + }, + { + 'label': 'image', + 'extensions': ['*.jpg'], + 'mimeTypes': ['image/jpg'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': true, + }, ); }); test('passes initialDirectory correctly', () async { await plugin.openFiles(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - 'multiple': true, - }), - ], + 'openFile', + arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': true, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.openFiles(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - 'multiple': true, - }), - ], + 'openFile', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': true, + }, ); }); @@ -228,21 +226,20 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'any', - 'extensions': ['*'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'any', + 'extensions': ['*'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }, ); }); }); @@ -267,57 +264,54 @@ void main() { await plugin .getSavePath(acceptedTypeGroups: [group, groupTwo]); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'text', - 'extensions': ['*.txt'], - 'mimeTypes': ['text/plain'], - }, - { - 'label': 'image', - 'extensions': ['*.jpg'], - 'mimeTypes': ['image/jpg'], - }, - ], - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], + 'getSavePath', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'text', + 'extensions': ['*.txt'], + 'mimeTypes': ['text/plain'], + }, + { + 'label': 'image', + 'extensions': ['*.jpg'], + 'mimeTypes': ['image/jpg'], + }, + ], + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': null, + }, ); }); test('passes initialDirectory correctly', () async { await plugin.getSavePath(initialDirectory: '/example/directory'); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'initialDirectory': '/example/directory', - 'suggestedName': null, - 'confirmButtonText': null, - }), - ], + 'getSavePath', + arguments: { + 'initialDirectory': '/example/directory', + 'suggestedName': null, + 'confirmButtonText': null, + }, ); }); test('passes confirmButtonText correctly', () async { await plugin.getSavePath(confirmButtonText: 'Open File'); - expect( + expectMethodCall( log, - [ - isMethodCall('getSavePath', arguments: { - 'initialDirectory': null, - 'suggestedName': null, - 'confirmButtonText': 'Open File', - }), - ], + 'getSavePath', + arguments: { + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': 'Open File', + }, ); }); @@ -339,21 +333,20 @@ void main() { await plugin.openFile(acceptedTypeGroups: [group]); - expect( + expectMethodCall( log, - [ - isMethodCall('openFile', arguments: { - 'acceptedTypeGroups': >[ - { - 'label': 'any', - 'extensions': ['*'], - }, - ], - 'initialDirectory': null, - 'confirmButtonText': null, - 'multiple': false, - }), - ], + 'openFile', + arguments: { + 'acceptedTypeGroups': >[ + { + 'label': 'any', + 'extensions': ['*'], + }, + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }, ); }); }); @@ -362,28 +355,83 @@ void main() { test('passes initialDirectory correctly', () async { await plugin.getDirectoryPath(initialDirectory: '/example/directory'); - expect( + expectMethodCall( + log, + 'getDirectoryPath', + arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + }, + ); + }); + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPath(confirmButtonText: 'Select Folder'); + + expectMethodCall( + log, + 'getDirectoryPath', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Select Folder', + }, + ); + }); + }); + + group('#getDirectoryPaths', () { + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPaths(initialDirectory: '/example/directory'); + + expectMethodCall( log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': '/example/directory', - 'confirmButtonText': null, - }), - ], + 'getDirectoryPath', + arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': true, + }, ); }); test('passes confirmButtonText correctly', () async { - await plugin.getDirectoryPath(confirmButtonText: 'Open File'); + await plugin.getDirectoryPaths( + confirmButtonText: 'Select one or mode folders'); + + expectMethodCall( + log, + 'getDirectoryPath', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Select one or mode folders', + 'multiple': true, + }, + ); + }); + test('passes multiple flag correctly', () async { + await plugin.getDirectoryPaths(); - expect( + expectMethodCall( log, - [ - isMethodCall('getDirectoryPath', arguments: { - 'initialDirectory': null, - 'confirmButtonText': 'Open File', - }), - ], + 'getDirectoryPath', + arguments: { + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': true, + }, ); }); }); } + +void expectMethodCall( + List log, + String methodName, { + Map? arguments, +}) { + expect(log, [isMethodCall(methodName, arguments: arguments)]); +} + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/file_selector/file_selector_macos/CHANGELOG.md b/packages/file_selector/file_selector_macos/CHANGELOG.md index 27d08ae26771..4fdab0b73b5d 100644 --- a/packages/file_selector/file_selector_macos/CHANGELOG.md +++ b/packages/file_selector/file_selector_macos/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + ## 0.9.0+4 * Converts platform channel to Pigeon. diff --git a/packages/file_selector/file_selector_macos/example/lib/get_directory_page.dart b/packages/file_selector/file_selector_macos/example/lib/get_directory_page.dart index a2a209dc9529..a3f6f6ab8798 100644 --- a/packages/file_selector/file_selector_macos/example/lib/get_directory_page.dart +++ b/packages/file_selector/file_selector_macos/example/lib/get_directory_page.dart @@ -21,10 +21,12 @@ class GetDirectoryPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(directoryPath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(directoryPath), + ); + } } @override diff --git a/packages/file_selector/file_selector_macos/example/lib/open_image_page.dart b/packages/file_selector/file_selector_macos/example/lib/open_image_page.dart index b6ada56ebb2b..9252d25f113c 100644 --- a/packages/file_selector/file_selector_macos/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector_macos/example/lib/open_image_page.dart @@ -28,10 +28,12 @@ class OpenImagePage extends StatelessWidget { final String fileName = file.name; final String filePath = file.path; - await showDialog( - context: context, - builder: (BuildContext context) => ImageDisplay(fileName, filePath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } } @override diff --git a/packages/file_selector/file_selector_macos/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_macos/example/lib/open_multiple_images_page.dart index c8e352a5b8bd..787717cdea13 100644 --- a/packages/file_selector/file_selector_macos/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector_macos/example/lib/open_multiple_images_page.dart @@ -32,10 +32,12 @@ class OpenMultipleImagesPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => MultipleImagesDisplay(files), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } } @override diff --git a/packages/file_selector/file_selector_macos/example/lib/open_text_page.dart b/packages/file_selector/file_selector_macos/example/lib/open_text_page.dart index 4c88d7475049..97812f2b3505 100644 --- a/packages/file_selector/file_selector_macos/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector_macos/example/lib/open_text_page.dart @@ -25,10 +25,12 @@ class OpenTextPage extends StatelessWidget { final String fileName = file.name; final String fileContent = await file.readAsString(); - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(fileName, fileContent), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } } @override diff --git a/packages/file_selector/file_selector_macos/example/pubspec.yaml b/packages/file_selector/file_selector_macos/example/pubspec.yaml index d3f3114bb481..a2122b2858b7 100644 --- a/packages/file_selector/file_selector_macos/example/pubspec.yaml +++ b/packages/file_selector/file_selector_macos/example/pubspec.yaml @@ -5,7 +5,7 @@ version: 1.0.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: file_selector_macos: diff --git a/packages/file_selector/file_selector_macos/pubspec.yaml b/packages/file_selector/file_selector_macos/pubspec.yaml index ac41a2525f4d..3654beaca4c0 100644 --- a/packages/file_selector/file_selector_macos/pubspec.yaml +++ b/packages/file_selector/file_selector_macos/pubspec.yaml @@ -6,7 +6,7 @@ version: 0.9.0+4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md index e0b08f086977..ae415ef8600d 100644 --- a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md +++ b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.4.0 * Adds `getDirectoryPaths` method to the interface. diff --git a/packages/file_selector/file_selector_platform_interface/pubspec.yaml b/packages/file_selector/file_selector_platform_interface/pubspec.yaml index 4ab63acbf7e6..b2461ee2a6d0 100644 --- a/packages/file_selector/file_selector_platform_interface/pubspec.yaml +++ b/packages/file_selector/file_selector_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 2.4.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: cross_file: ^0.3.0 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 9caa76c02bcb..c5438f7ecbc2 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 @@ -16,10 +16,15 @@ void main() { final List log = []; setUp(() { - plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return null; - }); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + plugin.channel, + (MethodCall methodCall) async { + log.add(methodCall); + return null; + }, + ); log.clear(); }); @@ -274,3 +279,9 @@ void expectMethodCall( }) { expect(log, [isMethodCall(methodName, arguments: arguments)]); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md index 5e531bb633d2..fbb58d61f999 100644 --- a/packages/file_selector/file_selector_web/CHANGELOG.md +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 0.9.0+2 * Changes XTypeGroup initialization from final to const. diff --git a/packages/file_selector/file_selector_web/example/pubspec.yaml b/packages/file_selector/file_selector_web/example/pubspec.yaml index e14f5c2eedea..985ce35f69a8 100644 --- a/packages/file_selector/file_selector_web/example/pubspec.yaml +++ b/packages/file_selector/file_selector_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: file_selector_platform_interface: ^2.2.0 diff --git a/packages/file_selector/file_selector_web/pubspec.yaml b/packages/file_selector/file_selector_web/pubspec.yaml index 848a41b754af..aceeb8b13693 100644 --- a/packages/file_selector/file_selector_web/pubspec.yaml +++ b/packages/file_selector/file_selector_web/pubspec.yaml @@ -6,7 +6,7 @@ version: 0.9.0+2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/file_selector/file_selector_windows/CHANGELOG.md b/packages/file_selector/file_selector_windows/CHANGELOG.md index 13e895ca46f1..1f9405d2c987 100644 --- a/packages/file_selector/file_selector_windows/CHANGELOG.md +++ b/packages/file_selector/file_selector_windows/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + ## 0.9.1+4 * Changes XTypeGroup initialization from final to const. diff --git a/packages/file_selector/file_selector_windows/example/lib/get_directory_page.dart b/packages/file_selector/file_selector_windows/example/lib/get_directory_page.dart index 0699dd121541..f6390ccef20d 100644 --- a/packages/file_selector/file_selector_windows/example/lib/get_directory_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/get_directory_page.dart @@ -21,10 +21,12 @@ class GetDirectoryPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(directoryPath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(directoryPath), + ); + } } @override diff --git a/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart b/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart index b6ada56ebb2b..9252d25f113c 100644 --- a/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/open_image_page.dart @@ -28,10 +28,12 @@ class OpenImagePage extends StatelessWidget { final String fileName = file.name; final String filePath = file.path; - await showDialog( - context: context, - builder: (BuildContext context) => ImageDisplay(fileName, filePath), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } } @override diff --git a/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart index c8e352a5b8bd..787717cdea13 100644 --- a/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/open_multiple_images_page.dart @@ -32,10 +32,12 @@ class OpenMultipleImagesPage extends StatelessWidget { // Operation was canceled by the user. return; } - await showDialog( - context: context, - builder: (BuildContext context) => MultipleImagesDisplay(files), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } } @override diff --git a/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart b/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart index 4c88d7475049..97812f2b3505 100644 --- a/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart +++ b/packages/file_selector/file_selector_windows/example/lib/open_text_page.dart @@ -25,10 +25,12 @@ class OpenTextPage extends StatelessWidget { final String fileName = file.name; final String fileContent = await file.readAsString(); - await showDialog( - context: context, - builder: (BuildContext context) => TextDisplay(fileName, fileContent), - ); + if (context.mounted) { + await showDialog( + context: context, + builder: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } } @override diff --git a/packages/file_selector/file_selector_windows/example/pubspec.yaml b/packages/file_selector/file_selector_windows/example/pubspec.yaml index bc886d32c896..d270c3067325 100644 --- a/packages/file_selector/file_selector_windows/example/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/example/pubspec.yaml @@ -5,7 +5,7 @@ version: 1.0.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: file_selector_platform_interface: ^2.2.0 diff --git a/packages/file_selector/file_selector_windows/pigeons/messages.dart b/packages/file_selector/file_selector_windows/pigeons/messages.dart index f2c9ab71bd82..c3b3aff192b8 100644 --- a/packages/file_selector/file_selector_windows/pigeons/messages.dart +++ b/packages/file_selector/file_selector_windows/pigeons/messages.dart @@ -6,7 +6,7 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', - dartTestOut: 'test/test_api.dart', + dartTestOut: 'test/test_api.g.dart', cppOptions: CppOptions(namespace: 'file_selector_windows'), cppHeaderOut: 'windows/messages.g.h', cppSourceOut: 'windows/messages.g.cpp', diff --git a/packages/file_selector/file_selector_windows/pubspec.yaml b/packages/file_selector/file_selector_windows/pubspec.yaml index ee0701b3fd30..a0a0f39fbd1f 100644 --- a/packages/file_selector/file_selector_windows/pubspec.yaml +++ b/packages/file_selector/file_selector_windows/pubspec.yaml @@ -6,7 +6,7 @@ version: 0.9.1+4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart index f07c9b67618d..62745f7df707 100644 --- a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart +++ b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.dart @@ -11,7 +11,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'file_selector_windows_test.mocks.dart'; -import 'test_api.dart'; +import 'test_api.g.dart'; @GenerateMocks([TestFileSelectorApi]) void main() { diff --git a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart index 61e17fcdfeaa..f60c92e6b7ee 100644 --- a/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart +++ b/packages/file_selector/file_selector_windows/test/file_selector_windows_test.mocks.dart @@ -5,7 +5,7 @@ import 'package:file_selector_windows/src/messages.g.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'test_api.dart' as _i2; +import 'test_api.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values diff --git a/packages/file_selector/file_selector_windows/test/test_api.dart b/packages/file_selector/file_selector_windows/test/test_api.g.dart similarity index 100% rename from packages/file_selector/file_selector_windows/test/test_api.dart rename to packages/file_selector/file_selector_windows/test/test_api.g.dart diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 81202f8159de..c169487f6a81 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,6 +1,6 @@ ## NEXT -* Updates minimum Flutter version to 2.10. +* Updates minimum Flutter version to 3.0. ## 2.0.7 diff --git a/packages/flutter_plugin_android_lifecycle/android/build.gradle b/packages/flutter_plugin_android_lifecycle/android/build.gradle index 5786a74e2e78..62c603262989 100644 --- a/packages/flutter_plugin_android_lifecycle/android/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/android/build.gradle @@ -30,10 +30,7 @@ android { consumerProguardFiles 'proguard.txt' } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } dependencies { @@ -56,6 +53,6 @@ android { dependencies { testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:5.1.1' } diff --git a/packages/flutter_plugin_android_lifecycle/example/android/gradle.properties b/packages/flutter_plugin_android_lifecycle/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/gradle.properties +++ b/packages/flutter_plugin_android_lifecycle/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml index e732497eee95..4c97e6c44cd1 100644 --- a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml @@ -4,6 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/flutter_plugin_android_lifecycle/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/pubspec.yaml index 3a6a2e017b53..4711d1c3629a 100644 --- a/packages/flutter_plugin_android_lifecycle/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.0.7 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 4287430c32ff..bab8412142d9 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.2.3 * Fixes a minor syntax error in `README.md`. diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/gradle.properties b/packages/google_maps_flutter/google_maps_flutter/example/android/gradle.properties index 207beb63fb48..c6c9db00b996 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/gradle.properties +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true 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 06bfbbf290e4..5813d42e617e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: cupertino_icons: ^1.0.5 diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 9e20b486bb67..0771314b9e44 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.2.3 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: 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 c6e71137903b..459e16b60c42 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 @@ -26,8 +26,12 @@ void main() { FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); }); setUp(() { @@ -194,3 +198,9 @@ void main() { expect(platformGoogleMap.circlesToAdd.isEmpty, true); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; 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 b2747c9cb5fe..2c6aba1bb0ba 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 @@ -13,7 +13,9 @@ class FakePlatformGoogleMap { : cameraPosition = CameraPosition.fromMap(params['initialCameraPosition']), channel = MethodChannel('plugins.flutter.io/google_maps_$id') { - channel.setMockMethodCallHandler(onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, onMethodCall); updateOptions(params['options'] as Map); updateMarkers(params); updatePolygons(params); @@ -478,3 +480,9 @@ Map? _decodeParams(Uint8List paramsMessage) { return const StandardMessageCodec().decodeMessage(messageBytes) as Map?; } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart index 0fc5e7723df5..99b12988f3b4 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 @@ -16,8 +16,12 @@ void main() { FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); }); setUp(() { @@ -576,3 +580,9 @@ void main() { expect(platformGoogleMap.buildingsEnabled, true); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart index b34fccbfa422..49b64b1b4b2a 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 @@ -29,8 +29,12 @@ void main() { ) async { // Inject two map widgets... await tester.pumpWidget( + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors Directionality( textDirection: TextDirection.ltr, + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors child: Column( children: const [ GoogleMap( 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 0acde97e8d9a..75a153e0eaa2 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 @@ -26,8 +26,12 @@ void main() { FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); }); setUp(() { @@ -200,3 +204,9 @@ void main() { expect(platformGoogleMap.markersToAdd.isEmpty, true); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; 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 1e5ea84a0422..152cbddfc34a 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 @@ -49,8 +49,12 @@ void main() { FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); }); setUp(() { @@ -403,3 +407,9 @@ void main() { expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; 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 4c68a71542d5..03b6c620190a 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 @@ -26,8 +26,12 @@ void main() { FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); }); setUp(() { @@ -216,3 +220,9 @@ void main() { expect(platformGoogleMap.polylinesToAdd.isEmpty, true); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart index b4586f743296..e4e4514dd501 100644 --- a/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart @@ -24,8 +24,12 @@ void main() { FakePlatformViewsController(); setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, + fakePlatformViewsController.fakePlatformViewsMethodHandler, + ); }); setUp(() { @@ -198,3 +202,9 @@ void main() { expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md index 66c5fcf85738..68b9f677e2db 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_android/CHANGELOG.md @@ -1,3 +1,16 @@ +## 2.4.5 + +* Fixes Initial padding not working when map has not been created yet. + +## 2.4.4 + +* Fixes Points losing precision when converting to LatLng. +* Updates minimum Flutter version to 3.0. + +## 2.4.3 + +* Updates code for stricter lint checks. + ## 2.4.2 * Updates code for stricter lint checks. diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle b/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle index 5b383fe3bc86..6f8d3060a9cf 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/build.gradle @@ -29,10 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } dependencies { @@ -42,7 +39,7 @@ android { androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:5.1.1' testImplementation 'androidx.test:core:1.2.0' testImplementation "org.robolectric:robolectric:4.3.1" } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java index 72c6959fe55e..22c8f4d24be6 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/Convert.java @@ -7,6 +7,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Point; +import androidx.annotation.VisibleForTesting; import com.google.android.gms.maps.CameraUpdate; import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.model.BitmapDescriptor; @@ -592,13 +593,14 @@ static String interpretCircleOptions(Object o, CircleOptionsSink sink) { } } - private static List toPoints(Object o) { + @VisibleForTesting + static List toPoints(Object o) { final List data = toList(o); final List points = new ArrayList<>(data.size()); for (Object rawPoint : data) { final List point = toList(rawPoint); - points.add(new LatLng(toFloat(point.get(0)), toFloat(point.get(1)))); + points.add(new LatLng(toDouble(point.get(0)), toDouble(point.get(1)))); } return points; } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index 66d3e283b8df..a57cd1a34c97 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java @@ -70,7 +70,7 @@ final class GoogleMapController private boolean trafficEnabled = false; private boolean buildingsEnabled = true; private boolean disposed = false; - private final float density; + @VisibleForTesting final float density; private MethodChannel.Result mapReadyResult; private final Context context; private final LifecycleProvider lifecycleProvider; @@ -84,6 +84,7 @@ final class GoogleMapController private List initialPolylines; private List initialCircles; private List> initialTileOverlays; + @VisibleForTesting List initialPadding; GoogleMapController( int id, @@ -209,6 +210,13 @@ public void onMapReady(GoogleMap googleMap) { updateInitialPolylines(); updateInitialCircles(); updateInitialTileOverlays(); + if (initialPadding != null && initialPadding.size() == 4) { + setPadding( + initialPadding.get(0), + initialPadding.get(1), + initialPadding.get(2), + initialPadding.get(3)); + } } @Override @@ -741,7 +749,22 @@ public void setPadding(float top, float left, float bottom, float right) { (int) (top * density), (int) (right * density), (int) (bottom * density)); + } else { + setInitialPadding(top, left, bottom, right); + } + } + + @VisibleForTesting + void setInitialPadding(float top, float left, float bottom, float right) { + if (initialPadding == null) { + initialPadding = new ArrayList<>(); + } else { + initialPadding.clear(); } + initialPadding.add(top); + initialPadding.add(left); + initialPadding.add(bottom); + initialPadding.add(right); } @Override diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java new file mode 100644 index 000000000000..0d635170c1f3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/ConvertTest.java @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.LatLng; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Test; + +public class ConvertTest { + + @Test + public void ConvertToPointsConvertsThePointsWithFullPrecision() { + double latitude = 43.03725568057; + double longitude = -87.90466904649; + ArrayList point = new ArrayList(); + point.add(latitude); + point.add(longitude); + ArrayList> pointsList = new ArrayList<>(); + pointsList.add(point); + List latLngs = Convert.toPoints(pointsList); + LatLng latLng = latLngs.get(0); + Assert.assertEquals(latitude, latLng.latitude, 1e-15); + Assert.assertEquals(longitude, latLng.longitude, 1e-15); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java index d8082b57e3db..52576962ba8d 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter_android/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java @@ -4,10 +4,12 @@ package io.flutter.plugins.googlemaps; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; 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 android.content.Context; @@ -20,6 +22,7 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import java.util.HashMap; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -145,4 +148,22 @@ public void MethodCalledAfterControllerIsDestroyed() throws InterruptedException argument.getValue().onMapLoaded(); verify(mapView, never()).invalidate(); } + + @Test + public void OnMapReadySetsPaddingIfInitialPaddingIsThere() { + float padding = 10f; + int paddingWithDensity = (int) (padding * googleMapController.density); + googleMapController.setInitialPadding(padding, padding, padding, padding); + googleMapController.onMapReady(mockGoogleMap); + verify(mockGoogleMap, times(1)) + .setPadding(paddingWithDensity, paddingWithDensity, paddingWithDensity, paddingWithDensity); + } + + @Test + public void SetPaddingStoresThePaddingValuesInInInitialPaddingWhenGoogleMapIsNull() { + assertNull(googleMapController.initialPadding); + googleMapController.setPadding(0f, 0f, 0f, 0f); + assertNotNull(googleMapController.initialPadding); + Assert.assertEquals(4, googleMapController.initialPadding.size()); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle.properties b/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle.properties index 207beb63fb48..c6c9db00b996 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle.properties +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml index a5e8bdb8bed6..aa29fa99a97b 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: cupertino_icons: ^1.0.5 diff --git a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart index 0188bb18fb20..0461b4cf71bc 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/lib/src/google_maps_flutter_android.dart @@ -539,7 +539,7 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { preferredRenderer = 'legacy'; break; case AndroidMapRenderer.platformDefault: - default: + case null: preferredRenderer = 'default'; } diff --git a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml index 492193e39f86..cf8bc81e7e7c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_maps_flutter_android description: Android implementation of the google_maps_flutter plugin. repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 2.4.2 +version: 2.4.5 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart index 431c2472945e..29c02c836a85 100644 --- a/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_android/test/google_maps_flutter_android_test.dart @@ -25,19 +25,24 @@ void main() { required int mapId, required Future? Function(MethodCall call) handler, }) { - maps - .ensureChannelInitialized(mapId) - .setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall.method); - return handler(methodCall); - }); + final MethodChannel channel = maps.ensureChannelInitialized(mapId); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (MethodCall methodCall) { + log.add(methodCall.method); + return handler(methodCall); + }, + ); } Future sendPlatformMessage( int mapId, String method, Map data) async { final ByteData byteData = const StandardMethodCodec().encodeMethodCall(MethodCall(method, data)); - await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage('plugins.flutter.dev/google_maps_android_$mapId', byteData, (ByteData? data) {}); } @@ -164,3 +169,9 @@ void main() { expect(widget, isA()); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md index fa135d8ff878..a65523f426c1 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.1.13 * Updates code for stricter lint checks. diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_coordinates.dart b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_coordinates.dart index 22f383bd1254..25247bc7c7bd 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_coordinates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/lib/map_coordinates.dart @@ -53,17 +53,28 @@ class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { _updateVisibleRegion(); return true; }, - child: ListView( + child: Stack( children: [ - Padding( - padding: const EdgeInsets.all(10.0), - child: Center( - child: SizedBox( - width: 300.0, - height: 200.0, - child: googleMap, + ListView( + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 200.0, + child: googleMap, + ), + ), ), - ), + // Add a block at the bottom of this list to allow validation that the visible region of the map + // does not change when scrolled under the safe view on iOS. + // https://github.com/flutter/flutter/issues/107913 + const SizedBox( + width: 300, + height: 1000, + ), + ], ), if (mapController != null) Center( @@ -71,13 +82,6 @@ class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { '\nnortheast: ${_visibleRegion.northeast},' '\nsouthwest: ${_visibleRegion.southwest}'), ), - // Add a block at the bottom of this list to allow validation that the visible region of the map - // does not change when scrolled under the safe view on iOS. - // https://github.com/flutter/flutter/issues/107913 - const SizedBox( - width: 300, - height: 1000, - ), ], ), ); diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml index 9c41fa57545e..ac27996fbc25 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: cupertino_icons: ^1.0.5 diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml index 579b24304cf4..c4f8d23cb382 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_ios/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.1.13 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart index 136481cf3abb..a5d376da1684 100644 --- a/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_ios/test/google_maps_flutter_ios_test.dart @@ -24,19 +24,24 @@ void main() { required int mapId, required Future? Function(MethodCall call) handler, }) { - maps - .ensureChannelInitialized(mapId) - .setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall.method); - return handler(methodCall); - }); + final MethodChannel channel = maps.ensureChannelInitialized(mapId); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (MethodCall methodCall) { + log.add(methodCall.method); + return handler(methodCall); + }, + ); } Future sendPlatformMessage( int mapId, String method, Map data) async { final ByteData byteData = const StandardMethodCodec().encodeMethodCall(MethodCall(method, data)); - await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage('plugins.flutter.dev/google_maps_ios_$mapId', byteData, (ByteData? data) {}); } @@ -122,3 +127,9 @@ void main() { equals('drag-end-marker')); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index 307b70016009..b3d6c5540e7a 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,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.2.5 * Updates code for stricter lint checks. 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 40b058ae6daf..6dfff89f8c4b 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 @@ -8,7 +8,7 @@ version: 2.2.5 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: collection: ^1.15.0 diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart index e5052184915f..ef37c2f221fa 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart @@ -24,19 +24,24 @@ void main() { required int mapId, required Future? Function(MethodCall call) handler, }) { - maps - .ensureChannelInitialized(mapId) - .setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall.method); - return handler(methodCall); - }); + final MethodChannel channel = maps.ensureChannelInitialized(mapId); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (MethodCall methodCall) { + log.add(methodCall.method); + return handler(methodCall); + }, + ); } Future sendPlatformMessage( int mapId, String method, Map data) async { final ByteData byteData = const StandardMethodCodec() .encodeMethodCall(MethodCall(method, data)); - await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage('plugins.flutter.io/google_maps_$mapId', byteData, (ByteData? data) {}); } @@ -120,3 +125,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index 3bbc08ac7186..42930348965f 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,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 0.4.0+5 + +* Updates code for stricter lint checks. + ## 0.4.0+4 * Updates code for stricter lint checks. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml index 35c9c903e982..43f67946464a 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none # Tests require flutter beta or greater to run. environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: 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 d5f27ee05d0c..25cba849475b 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 @@ -98,9 +98,15 @@ gmaps.MapTypeId _gmapTypeIDForPluginType(MapType type) { return gmaps.MapTypeId.HYBRID; case MapType.normal: case MapType.none: - default: return gmaps.MapTypeId.ROADMAP; } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + // ignore: dead_code + return gmaps.MapTypeId.ROADMAP; } gmaps.MapOptions _applyInitialPosition( 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 6278ab01ba6c..072d584b133f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -2,11 +2,11 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter repository: https://github.com/flutter/plugins/tree/main/packages/google_maps_flutter/google_maps_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 -version: 0.4.0+4 +version: 0.4.0+5 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index c7ddb6ba9345..f6b1e5790cc4 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,20 @@ +## 6.0.0 + +* **Breaking change** for platform `web`: + * Endorses `google_sign_in_web: ^0.11.0` as the web implementation of the plugin. + * The web package is now backed by the **Google Identity Services (GIS) SDK**, + instead of the **Google Sign-In for Web JS SDK**, which is set to be deprecated + after March 31, 2023. + * Migration information can be found in the + [`google_sign_in_web` package README](https://pub.dev/packages/google_sign_in_web). + +For every platform other than `web`, this version should be identical to `5.4.4`. + +## 5.4.4 + +* Adds documentation for iOS auth with SERVER_CLIENT_ID +* Updates minimum Flutter version to 3.0. + ## 5.4.3 * Updates code for stricter lint checks. diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index e467ca8541b9..6961bc67b7df 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -43,7 +43,13 @@ This plugin requires iOS 9.0 or higher. 5. Select `GoogleService-Info.plist` from the file manager. 6. A dialog will show up and ask you to select the targets, select the `Runner` target. -7. Then add the `CFBundleURLTypes` attributes below into the +7. If you need to authenticate to a backend server you can add a + `SERVER_CLIENT_ID` key value pair in your `GoogleService-Info.plist`. + ```xml + SERVER_CLIENT_ID + [YOUR SERVER CLIENT ID] + ``` +8. Then add the `CFBundleURLTypes` attributes below into the `[my_project]/ios/Runner/Info.plist` file. ```xml @@ -65,9 +71,9 @@ This plugin requires iOS 9.0 or higher. ``` -As an alternative to adding `GoogleService-Info.plist` to your Xcode project, you can instead -configure your app in Dart code. In this case, skip steps 3-6 and pass `clientId` and -`serverClientId` to the `GoogleSignIn` constructor: +As an alternative to adding `GoogleService-Info.plist` to your Xcode project, +you can instead configure your app in Dart code. In this case, skip steps 3 to 7 + and pass `clientId` and `serverClientId` to the `GoogleSignIn` constructor: ```dart GoogleSignIn _googleSignIn = GoogleSignIn( @@ -79,7 +85,7 @@ GoogleSignIn _googleSignIn = GoogleSignIn( ); ``` -Note that step 7 is still required. +Note that step 8 is still required. #### iOS additional requirement diff --git a/packages/google_sign_in/google_sign_in/example/android/gradle.properties b/packages/google_sign_in/google_sign_in/example/android/gradle.properties index d12b9a8297e5..5c693e744274 100644 --- a/packages/google_sign_in/google_sign_in/example/android/gradle.properties +++ b/packages/google_sign_in/google_sign_in/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true 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 fbf8f7cf0591..f1cd3828bd87 100644 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index f6e1faf12089..ec61a31598d7 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,12 +3,11 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.4.3 - +version: 6.0.0 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -26,7 +25,7 @@ dependencies: google_sign_in_android: ^6.1.0 google_sign_in_ios: ^5.5.0 google_sign_in_platform_interface: ^2.2.0 - google_sign_in_web: ^0.10.0 + google_sign_in_web: ^0.11.0 dev_dependencies: build_runner: ^2.1.10 diff --git a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md index 9775c409de43..6ce3cb97e8db 100644 --- a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md @@ -1,3 +1,12 @@ +## 6.1.6 + +* Minor implementation cleanup +* Updates minimum Flutter version to 3.0. + +## 6.1.5 + +* Updates play-services-auth version to 20.4.1. + ## 6.1.4 * Rolls Guava to version 31.1. diff --git a/packages/google_sign_in/google_sign_in_android/android/build.gradle b/packages/google_sign_in/google_sign_in_android/android/build.gradle index 19d43aa3dee4..21b7fa178c8f 100644 --- a/packages/google_sign_in/google_sign_in_android/android/build.gradle +++ b/packages/google_sign_in/google_sign_in_android/android/build.gradle @@ -29,10 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } @@ -50,8 +47,8 @@ android { } dependencies { - implementation 'com.google.android.gms:play-services-auth:20.4.0' + implementation 'com.google.android.gms:play-services-auth:20.4.1' implementation 'com.google.guava:guava:31.1-android' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'org.mockito:mockito-inline:5.0.0' } diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index d345d4976c63..8963a5169e89 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -404,9 +404,9 @@ public void init( public void signInSilently(Result result) { checkAndSetPendingOperation(METHOD_SIGN_IN_SILENTLY, result); Task task = signInClient.silentSignIn(); - if (task.isSuccessful()) { + if (task.isComplete()) { // There's immediate result available. - onSignInAccount(task.getResult()); + onSignInResult(task); } else { task.addOnCompleteListener( new OnCompleteListener() { @@ -516,7 +516,7 @@ private void onSignInResult(Task completedTask) { GoogleSignInAccount account = completedTask.getResult(ApiException.class); onSignInAccount(account); } catch (ApiException e) { - // Forward all errors and let Dart side decide how to handle. + // Forward all errors and let Dart decide how to handle. String errorCode = errorCodeForStatus(e.getStatusCode()); finishWithError(errorCode, e.toString()); } catch (RuntimeExecutionException e) { @@ -538,14 +538,20 @@ private void onSignInAccount(GoogleSignInAccount account) { } private String errorCodeForStatus(int statusCode) { - if (statusCode == GoogleSignInStatusCodes.SIGN_IN_CANCELLED) { - return ERROR_REASON_SIGN_IN_CANCELED; - } else if (statusCode == CommonStatusCodes.SIGN_IN_REQUIRED) { - return ERROR_REASON_SIGN_IN_REQUIRED; - } else if (statusCode == CommonStatusCodes.NETWORK_ERROR) { - return ERROR_REASON_NETWORK_ERROR; - } else { - return ERROR_REASON_SIGN_IN_FAILED; + switch (statusCode) { + case GoogleSignInStatusCodes.SIGN_IN_CANCELLED: + return ERROR_REASON_SIGN_IN_CANCELED; + case CommonStatusCodes.SIGN_IN_REQUIRED: + return ERROR_REASON_SIGN_IN_REQUIRED; + case CommonStatusCodes.NETWORK_ERROR: + return ERROR_REASON_NETWORK_ERROR; + case GoogleSignInStatusCodes.SIGN_IN_CURRENTLY_IN_PROGRESS: + case GoogleSignInStatusCodes.SIGN_IN_FAILED: + case CommonStatusCodes.INVALID_ACCOUNT: + case CommonStatusCodes.INTERNAL_ERROR: + return ERROR_REASON_SIGN_IN_FAILED; + default: + return ERROR_REASON_SIGN_IN_FAILED; } } diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index 9692417390a5..78568460c9e6 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -17,7 +17,11 @@ import com.google.android.gms.auth.api.signin.GoogleSignInAccount; import com.google.android.gms.auth.api.signin.GoogleSignInClient; import com.google.android.gms.auth.api.signin.GoogleSignInOptions; +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.common.api.Scope; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.tasks.Task; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -43,6 +47,7 @@ public class GoogleSignInTest { @Mock GoogleSignInWrapper mockGoogleSignIn; @Mock GoogleSignInAccount account; @Mock GoogleSignInClient mockClient; + @Mock Task mockSignInTask; private GoogleSignInPlugin plugin; @Before @@ -204,6 +209,27 @@ public void signInThrowsWithoutActivity() { plugin.onMethodCall(new MethodCall("signIn", null), null); } + @Test + public void signInSilentlyThatImmediatelyCompletesWithoutResultFinishesWithError() + throws ApiException { + final String clientId = "fakeClientId"; + MethodCall methodCall = buildInitMethodCall(clientId, null); + initAndAssertServerClientId(methodCall, clientId); + + ApiException exception = + new ApiException(new Status(CommonStatusCodes.SIGN_IN_REQUIRED, "Error text")); + when(mockClient.silentSignIn()).thenReturn(mockSignInTask); + when(mockSignInTask.isComplete()).thenReturn(true); + when(mockSignInTask.getResult(ApiException.class)).thenThrow(exception); + + plugin.onMethodCall(new MethodCall("signInSilently", null), result); + verify(result) + .error( + "sign_in_required", + "com.google.android.gms.common.api.ApiException: 4: Error text", + null); + } + @Test public void init_LoadsServerClientIdFromResources() { final String packageName = "fakePackageName"; diff --git a/packages/google_sign_in/google_sign_in_android/example/android/gradle.properties b/packages/google_sign_in/google_sign_in_android/example/android/gradle.properties index d12b9a8297e5..5c693e744274 100644 --- a/packages/google_sign_in/google_sign_in_android/example/android/gradle.properties +++ b/packages/google_sign_in/google_sign_in_android/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true diff --git a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml index 5ac2240cbba1..72d8b82a9bf5 100644 --- a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml index 350fe450f940..4be89f27286a 100644 --- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -2,11 +2,11 @@ name: google_sign_in_android description: Android implementation of the google_sign_in plugin. repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 6.1.4 +version: 6.1.6 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart index 2565836f51aa..b70d2e7bffa6 100644 --- a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart +++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart @@ -50,14 +50,19 @@ void main() { setUp(() { responses = Map.from(kDefaultResponses); - channel.setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall); - final dynamic response = responses[methodCall.method]; - if (response != null && response is Exception) { - return Future.error('$response'); - } - return Future.value(response); - }); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (MethodCall methodCall) { + log.add(methodCall); + final dynamic response = responses[methodCall.method]; + if (response != null && response is Exception) { + return Future.error('$response'); + } + return Future.value(response); + }, + ); log.clear(); }); @@ -159,3 +164,9 @@ void main() { expect(log, tests.values); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md index 7c5ebc097568..495d268bde03 100644 --- a/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 5.5.1 * Fixes passing `serverClientId` via the channelled `init` call diff --git a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml index aedc4b01aade..e2e643d1805d 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml index 04998d8945b4..69884ca0fe70 100644 --- a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml @@ -6,7 +6,7 @@ version: 5.5.1 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart index 0fee1af66120..6adbdec39b74 100644 --- a/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart +++ b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart @@ -51,14 +51,19 @@ void main() { setUp(() { responses = Map.from(kDefaultResponses); log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall); - final dynamic response = responses[methodCall.method]; - if (response != null && response is Exception) { - return Future.error('$response'); - } - return Future.value(response); - }); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + channel, + (MethodCall methodCall) { + log.add(methodCall); + final dynamic response = responses[methodCall.method]; + if (response != null && response is Exception) { + return Future.error('$response'); + } + return Future.value(response); + }, + ); }); test('registered instance', () { @@ -162,3 +167,9 @@ void main() { expect(log, tests.values); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md index 01d54b23dae0..8adba8aa966f 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,6 +1,6 @@ ## NEXT -* Updates minimum Flutter version to 2.10. +* Updates minimum Flutter version to 3.0. ## 2.3.0 diff --git a/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml index 0902069364ce..936257b9d817 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 @@ -8,7 +8,7 @@ version: 2.3.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: 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 0972d0be4855..0837f6d5d02c 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 @@ -50,7 +50,9 @@ void main() { setUp(() { responses = Map.from(kDefaultResponses); - channel.setMockMethodCallHandler((MethodCall methodCall) { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) { log.add(methodCall); final dynamic response = responses[methodCall.method]; if (response != null && response is Exception) { @@ -160,3 +162,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md index c5c57992a997..015334d77a59 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,10 @@ +## 0.11.0 + +* **Breaking Change:** Migrates JS-interop to `package:google_identity_services_web` + * Uses the new Google Identity Authentication and Authorization JS SDKs. [Docs](https://developers.google.com/identity). + * Added "Migrating to v0.11" section to the `README.md`. +* Updates minimum Flutter version to 3.0. + ## 0.10.2+1 * Updates code for `no_leading_underscores_for_local_identifiers` lint. diff --git a/packages/google_sign_in/google_sign_in_web/README.md b/packages/google_sign_in/google_sign_in_web/README.md index 7c02379808da..64bfd7a20161 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -2,6 +2,122 @@ The web implementation of [google_sign_in](https://pub.dev/packages/google_sign_in) +## Migrating to v0.11 (Google Identity Services) + +The `google_sign_in_web` plugin is backed by the new Google Identity Services +(GIS) JS SDK since version 0.11.0. + +The GIS SDK is used both for [Authentication](https://developers.google.com/identity/gsi/web/guides/overview) +and [Authorization](https://developers.google.com/identity/oauth2/web/guides/overview) flows. + +The GIS SDK, however, doesn't behave exactly like the one being deprecated. +Some concepts have experienced pretty drastic changes, and that's why this +plugin required a major version update. + +### Differences between Google Identity Services SDK and Google Sign-In for Web SDK. + +The **Google Sign-In JavaScript for Web JS SDK** is set to be deprecated after +March 31, 2023. **Google Identity Services (GIS) SDK** is the new solution to +quickly and easily sign users into your app suing their Google accounts. + +* In the GIS SDK, Authentication and Authorization are now two separate concerns. + * Authentication (information about the current user) flows will not + authorize `scopes` anymore. + * Authorization (permissions for the app to access certain user information) + flows will not return authentication information. +* The GIS SDK no longer has direct access to previously-seen users upon initialization. + * `signInSilently` now displays the One Tap UX for web. +* The GIS SDK only provides an `idToken` (JWT-encoded info) when the user + successfully completes an authentication flow. In the plugin: `signInSilently`. +* The plugin `signIn` method uses the Oauth "Implicit Flow" to Authorize the requested `scopes`. + * If the user hasn't `signInSilently`, they'll have to sign in as a first step + of the Authorization popup flow. + * If `signInSilently` was unsuccessful, the plugin will add extra `scopes` to + `signIn` and retrieve basic Profile information from the People API via a + REST call immediately after a successful authorization. In this case, the + `idToken` field of the `GoogleSignInUserData` will always be null. +* The GIS SDK no longer handles sign-in state and user sessions, it only provides + Authentication credentials for the moment the user did authenticate. +* The GIS SDK no longer is able to renew Authorization sessions on the web. + Once the token expires, API requests will begin to fail with unauthorized, + and user Authorization is required again. + +See more differences in the following migration guides: + +* Authentication > [Migrating from Google Sign-In](https://developers.google.com/identity/gsi/web/guides/migration) +* Authorization > [Migrate to Google Identity Services](https://developers.google.com/identity/oauth2/web/guides/migration-to-gis) + +### New use cases to take into account in your app + +#### Enable access to the People API for your GCP project + +Since the GIS SDK is separating Authentication from Authorization, the +[Oauth Implicit pop-up flow](https://developers.google.com/identity/oauth2/web/guides/use-token-model) +used to Authorize scopes does **not** return any Authentication information +anymore (user credential / `idToken`). + +If the plugin is not able to Authenticate an user from `signInSilently` (the +OneTap UX flow), it'll add extra `scopes` to those requested by the programmer +so it can perform a [People API request](https://developers.google.com/people/api/rest/v1/people/get) +to retrieve basic profile information about the user that is signed-in. + +The information retrieved from the People API is used to complete data for the +[`GoogleSignInAccount`](https://pub.dev/documentation/google_sign_in/latest/google_sign_in/GoogleSignInAccount-class.html) +object that is returned after `signIn` completes successfully. + +#### `signInSilently` always returns `null` + +Previous versions of this plugin were able to return a `GoogleSignInAccount` +object that was fully populated (signed-in and authorized) from `signInSilently` +because the former SDK equated "is authenticated" and "is authorized". + +With the GIS SDK, `signInSilently` only deals with user Authentication, so users +retrieved "silently" will only contain an `idToken`, but not an `accessToken`. + +Only after `signIn` or `requestScopes`, a user will be fully formed. + +The GIS-backed plugin always returns `null` from `signInSilently`, to force apps +that expect the former logic to perform a full `signIn`, which will result in a +fully Authenticated and Authorized user, and making this migration easier. + +#### `idToken` is `null` in the `GoogleSignInAccount` object after `signIn` + +Since the GIS SDK is separating Authentication and Authorization, when a user +fails to Authenticate through `signInSilently` and the plugin performs the +fallback request to the People API described above, +the returned `GoogleSignInUserData` object will contain basic profile information +(name, email, photo, ID), but its `idToken` will be `null`. + +This is because JWT are cryptographically signed by Google Identity Services, and +this plugin won't spoof that signature when it retrieves the information from a +simple REST request. + +#### User Sessions + +Since the GIS SDK does _not_ manage user sessions anymore, apps that relied on +this feature might break. + +If long-lived sessions are required, consider using some user authentication +system that supports Google Sign In as a federated Authentication provider, +like [Firebase Auth](https://firebase.google.com/docs/auth/flutter/federated-auth#google), +or similar. + +#### Expired / Invalid Authorization Tokens + +Since the GIS SDK does _not_ auto-renew authorization tokens anymore, it's now +the responsibility of your app to do so. + +Apps now need to monitor the status code of their REST API requests for response +codes different to `200`. For example: + +* `401`: Missing or invalid access token. +* `403`: Expired access token. + +In either case, your app needs to prompt the end user to `signIn` or +`requestScopes`, to interactively renew the token. + +The GIS SDK limits authorization token duration to one hour (3600 seconds). + ## Usage ### Import the package @@ -12,7 +128,7 @@ normally. This package will be automatically included in your app when you do. ### Web integration -First, go through the instructions [here](https://developers.google.com/identity/sign-in/web/sign-in#before_you_begin) to create your Google Sign-In OAuth client ID. +First, go through the instructions [here](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid) to create your Google Sign-In OAuth client ID. On your `web/index.html` file, add the following `meta` tag, somewhere in the `head` of the document: @@ -29,7 +145,10 @@ You can do this by: 2. Clicking "Edit" in the OAuth 2.0 Web application client that you created above. 3. Adding the URIs you want to the **Authorized JavaScript origins**. -For local development, may add a `localhost` entry, for example: `http://localhost:7357` +For local development, you must add two `localhost` entries: + +* `http://localhost` and +* `http://localhost:7357` (or any port that is free in your machine) #### Starting flutter in http://localhost:7357 @@ -45,40 +164,11 @@ flutter run -d chrome --web-hostname localhost --web-port 7357 Read the rest of the instructions if you need to add extra APIs (like Google People API). - ### Using the plugin -Add the following import to your Dart code: - -```dart -import 'package:google_sign_in/google_sign_in.dart'; -``` - -Initialize GoogleSignIn with the scopes you want: -```dart -GoogleSignIn _googleSignIn = GoogleSignIn( - scopes: [ - 'email', - 'https://www.googleapis.com/auth/contacts.readonly', - ], -); -``` - -[Full list of available scopes](https://developers.google.com/identity/protocols/googlescopes). - -Note that the `serverClientId` parameter of the `GoogleSignIn` constructor is not supported on Web. +See the [**Usage** instructions of `package:google_sign_in`](https://pub.dev/packages/google_sign_in#usage) -You can now use the `GoogleSignIn` class to authenticate in your Dart code, e.g. - -```dart -Future _handleSignIn() async { - try { - await _googleSignIn.signIn(); - } catch (error) { - print(error); - } -} -``` +Note that the **`serverClientId` parameter of the `GoogleSignIn` constructor is not supported on Web.** ## Example @@ -86,7 +176,7 @@ Find the example wiring in the [Google sign-in example application](https://gith ## API details -See the [google_sign_in.dart](https://github.com/flutter/plugins/blob/main/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart) for more API details. +See [google_sign_in.dart](https://github.com/flutter/plugins/blob/main/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart) for more API details. ## Contributions and Testing diff --git a/packages/google_sign_in/google_sign_in_web/example/build.yaml b/packages/google_sign_in/google_sign_in_web/example/build.yaml new file mode 100644 index 000000000000..db3104bb04c6 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - integration_test/*.dart + - lib/$lib$ + - $package$ diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart deleted file mode 100644 index 5dada90397fa..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// This file is a copy of `auth2_test.dart`, before it was migrated to the -// new `initWithParams` method, and is kept to ensure test coverage of the -// deprecated `init` method, until it is removed. - -import 'dart:html' as html; - -import 'package:flutter/services.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'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - final GoogleSignInTokenData expectedTokenData = - GoogleSignInTokenData(idToken: '70k3n', accessToken: 'access_70k3n'); - - final GoogleSignInUserData expectedUserData = GoogleSignInUserData( - displayName: 'Foo Bar', - email: 'foo@example.com', - id: '123', - photoUrl: 'http://example.com/img.jpg', - idToken: expectedTokenData.idToken, - ); - - late GoogleSignInPlugin plugin; - - group('plugin.initialize() throws a catchable exception', () { - setUp(() { - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2InitError()); - plugin = GoogleSignInPlugin(); - }); - - testWidgets('initialize throws PlatformException', - (WidgetTester tester) async { - await expectLater( - plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ), - throwsA(isA())); - }); - - testWidgets('initialize forwards error code from JS', - (WidgetTester tester) async { - try { - await plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ); - fail('plugin.initialize should have thrown an exception!'); - } catch (e) { - final String code = js_util.getProperty(e, 'code'); - expect(code, 'idpiframe_initialization_failed'); - } - }); - }); - - group('other methods also throw catchable exceptions on initialize fail', () { - // This function ensures that initialize gets called, but for some reason, - // we ignored that it has thrown stuff... - Future discardInit() async { - try { - await plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ); - } catch (e) { - // Noop so we can call other stuff - } - } - - setUp(() { - gapiUrl = toBase64Url(gapi_mocks.auth2InitError()); - plugin = GoogleSignInPlugin(); - }); - - testWidgets('signInSilently throws', (WidgetTester tester) async { - await discardInit(); - await expectLater( - plugin.signInSilently(), throwsA(isA())); - }); - - testWidgets('signIn throws', (WidgetTester tester) async { - await discardInit(); - await expectLater(plugin.signIn(), throwsA(isA())); - }); - - testWidgets('getTokens throws', (WidgetTester tester) async { - await discardInit(); - await expectLater(plugin.getTokens(email: 'test@example.com'), - throwsA(isA())); - }); - testWidgets('requestScopes', (WidgetTester tester) async { - await discardInit(); - await expectLater(plugin.requestScopes(['newScope']), - throwsA(isA())); - }); - }); - - group('auth2 Init Successful', () { - setUp(() { - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess(expectedUserData)); - plugin = GoogleSignInPlugin(); - }); - - testWidgets('Init requires clientId', (WidgetTester tester) async { - expect(plugin.init(hostedDomain: ''), throwsAssertionError); - }); - - testWidgets("Init doesn't accept spaces in scopes", - (WidgetTester tester) async { - expect( - plugin.init( - hostedDomain: '', - clientId: '', - scopes: ['scope with spaces'], - ), - throwsAssertionError); - }); - - // See: https://github.com/flutter/flutter/issues/88084 - testWidgets('Init passes plugin_name parameter with the expected value', - (WidgetTester tester) async { - await plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ); - - final Object? initParameters = - js_util.getProperty(html.window, 'gapi2.init.parameters'); - expect(initParameters, isNotNull); - - final Object? pluginNameParameter = - js_util.getProperty(initParameters!, 'plugin_name'); - expect(pluginNameParameter, isA()); - expect(pluginNameParameter, 'dart-google_sign_in_web'); - }); - - group('Successful .initialize, then', () { - setUp(() async { - await plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ); - await plugin.initialized; - }); - - testWidgets('signInSilently', (WidgetTester tester) async { - final GoogleSignInUserData actualUser = - (await plugin.signInSilently())!; - - expect(actualUser, expectedUserData); - }); - - testWidgets('signIn', (WidgetTester tester) async { - final GoogleSignInUserData actualUser = (await plugin.signIn())!; - - expect(actualUser, expectedUserData); - }); - - testWidgets('getTokens', (WidgetTester tester) async { - final GoogleSignInTokenData actualToken = - await plugin.getTokens(email: expectedUserData.email); - - expect(actualToken, expectedTokenData); - }); - - testWidgets('requestScopes', (WidgetTester tester) async { - final bool scopeGranted = - await plugin.requestScopes(['newScope']); - - expect(scopeGranted, isTrue); - }); - }); - }); - - group('auth2 Init successful, but exception on signIn() method', () { - setUp(() async { - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2SignInError()); - plugin = GoogleSignInPlugin(); - await plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ); - await plugin.initialized; - }); - - testWidgets('User aborts sign in flow, throws PlatformException', - (WidgetTester tester) async { - await expectLater(plugin.signIn(), throwsA(isA())); - }); - - testWidgets('User aborts sign in flow, error code is forwarded from JS', - (WidgetTester tester) async { - try { - await plugin.signIn(); - fail('plugin.signIn() should have thrown an exception!'); - } catch (e) { - final String code = js_util.getProperty(e, 'code'); - expect(code, 'popup_closed_by_user'); - } - }); - }); -} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart deleted file mode 100644 index 3e803b83fa0c..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:html' as html; - -import 'package:flutter/services.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'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - final GoogleSignInTokenData expectedTokenData = - GoogleSignInTokenData(idToken: '70k3n', accessToken: 'access_70k3n'); - - final GoogleSignInUserData expectedUserData = GoogleSignInUserData( - displayName: 'Foo Bar', - email: 'foo@example.com', - id: '123', - photoUrl: 'http://example.com/img.jpg', - idToken: expectedTokenData.idToken, - ); - - late GoogleSignInPlugin plugin; - - group('plugin.initWithParams() throws a catchable exception', () { - setUp(() { - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2InitError()); - plugin = GoogleSignInPlugin(); - }); - - testWidgets('throws PlatformException', (WidgetTester tester) async { - await expectLater( - plugin.initWithParams(const SignInInitParameters( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - )), - throwsA(isA())); - }); - - testWidgets('forwards error code from JS', (WidgetTester tester) async { - try { - await plugin.initWithParams(const SignInInitParameters( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - )); - fail('plugin.initWithParams should have thrown an exception!'); - } catch (e) { - final String code = js_util.getProperty(e, 'code'); - expect(code, 'idpiframe_initialization_failed'); - } - }); - }); - - group('other methods also throw catchable exceptions on initWithParams fail', - () { - // This function ensures that initWithParams gets called, but for some - // reason, we ignored that it has thrown stuff... - Future discardInit() async { - try { - await plugin.initWithParams(const SignInInitParameters( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - )); - } catch (e) { - // Noop so we can call other stuff - } - } - - setUp(() { - gapiUrl = toBase64Url(gapi_mocks.auth2InitError()); - plugin = GoogleSignInPlugin(); - }); - - testWidgets('signInSilently throws', (WidgetTester tester) async { - await discardInit(); - await expectLater( - plugin.signInSilently(), throwsA(isA())); - }); - - testWidgets('signIn throws', (WidgetTester tester) async { - await discardInit(); - await expectLater(plugin.signIn(), throwsA(isA())); - }); - - testWidgets('getTokens throws', (WidgetTester tester) async { - await discardInit(); - await expectLater(plugin.getTokens(email: 'test@example.com'), - throwsA(isA())); - }); - testWidgets('requestScopes', (WidgetTester tester) async { - await discardInit(); - await expectLater(plugin.requestScopes(['newScope']), - throwsA(isA())); - }); - }); - - group('auth2 Init Successful', () { - setUp(() { - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess(expectedUserData)); - plugin = GoogleSignInPlugin(); - }); - - testWidgets('Init requires clientId', (WidgetTester tester) async { - expect( - plugin.initWithParams(const SignInInitParameters(hostedDomain: '')), - throwsAssertionError); - }); - - testWidgets("Init doesn't accept serverClientId", - (WidgetTester tester) async { - expect( - plugin.initWithParams(const SignInInitParameters( - clientId: '', - serverClientId: '', - )), - throwsAssertionError); - }); - - testWidgets("Init doesn't accept spaces in scopes", - (WidgetTester tester) async { - expect( - plugin.initWithParams(const SignInInitParameters( - hostedDomain: '', - clientId: '', - scopes: ['scope with spaces'], - )), - throwsAssertionError); - }); - - // See: https://github.com/flutter/flutter/issues/88084 - testWidgets('Init passes plugin_name parameter with the expected value', - (WidgetTester tester) async { - await plugin.initWithParams(const SignInInitParameters( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - )); - - final Object? initParameters = - js_util.getProperty(html.window, 'gapi2.init.parameters'); - expect(initParameters, isNotNull); - - final Object? pluginNameParameter = - js_util.getProperty(initParameters!, 'plugin_name'); - expect(pluginNameParameter, isA()); - expect(pluginNameParameter, 'dart-google_sign_in_web'); - }); - - group('Successful .initWithParams, then', () { - setUp(() async { - await plugin.initWithParams(const SignInInitParameters( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - )); - await plugin.initialized; - }); - - testWidgets('signInSilently', (WidgetTester tester) async { - final GoogleSignInUserData actualUser = - (await plugin.signInSilently())!; - - expect(actualUser, expectedUserData); - }); - - testWidgets('signIn', (WidgetTester tester) async { - final GoogleSignInUserData actualUser = (await plugin.signIn())!; - - expect(actualUser, expectedUserData); - }); - - testWidgets('getTokens', (WidgetTester tester) async { - final GoogleSignInTokenData actualToken = - await plugin.getTokens(email: expectedUserData.email); - - expect(actualToken, expectedTokenData); - }); - - testWidgets('requestScopes', (WidgetTester tester) async { - final bool scopeGranted = - await plugin.requestScopes(['newScope']); - - expect(scopeGranted, isTrue); - }); - }); - }); - - group('auth2 Init successful, but exception on signIn() method', () { - setUp(() async { - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2SignInError()); - plugin = GoogleSignInPlugin(); - await plugin.initWithParams(const SignInInitParameters( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - )); - await plugin.initialized; - }); - - testWidgets('User aborts sign in flow, throws PlatformException', - (WidgetTester tester) async { - await expectLater(plugin.signIn(), throwsA(isA())); - }); - - testWidgets('User aborts sign in flow, error code is forwarded from JS', - (WidgetTester tester) async { - try { - await plugin.signIn(); - fail('plugin.signIn() should have thrown an exception!'); - } catch (e) { - final String code = js_util.getProperty(e, 'code'); - expect(code, 'popup_closed_by_user'); - } - }); - }); -} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart deleted file mode 100644 index 7bfef53f7a23..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// This file is a copy of `gapi_load_test.dart`, before it was migrated to the -// new `initWithParams` method, and is kept to ensure test coverage of the -// deprecated `init` method, until it is removed. - -import 'dart:html' as html; - -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(email: 'test@test.com', id: '1234'))); - - testWidgets('Plugin is initialized after GAPI fully loads and init is called', - (WidgetTester tester) async { - expect( - html.querySelector('script[src^="data:"]'), - isNull, - reason: 'Mock script not present before instantiating the plugin', - ); - final GoogleSignInPlugin plugin = GoogleSignInPlugin(); - expect( - html.querySelector('script[src^="data:"]'), - isNotNull, - reason: 'Mock script should be injected', - ); - expect(() { - plugin.initialized; - }, throwsStateError, - reason: - 'The plugin should throw if checking for `initialized` before calling .init'); - await plugin.init(hostedDomain: '', clientId: ''); - await plugin.initialized; - expect( - plugin.initialized, - completes, - reason: 'The plugin should complete the future once initialized.', - ); - }); -} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart deleted file mode 100644 index fc753e20d92c..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:html' as html; - -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(email: 'test@test.com', id: '1234'))); - - testWidgets('Plugin is initialized after GAPI fully loads and init is called', - (WidgetTester tester) async { - expect( - html.querySelector('script[src^="data:"]'), - isNull, - reason: 'Mock script not present before instantiating the plugin', - ); - final GoogleSignInPlugin plugin = GoogleSignInPlugin(); - expect( - html.querySelector('script[src^="data:"]'), - isNotNull, - reason: 'Mock script should be injected', - ); - expect(() { - plugin.initialized; - }, throwsStateError, - reason: 'The plugin should throw if checking for `initialized` before ' - 'calling .initWithParams'); - await plugin.initWithParams(const SignInInitParameters( - hostedDomain: '', - clientId: '', - )); - await plugin.initialized; - expect( - plugin.initialized, - completes, - reason: 'The plugin should complete the future once initialized.', - ); - }); -} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart deleted file mode 100644 index 43eb9a55d06b..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -library gapi_mocks; - -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; - -import 'src/gapi.dart'; -import 'src/google_user.dart'; -import 'src/test_iife.dart'; - -part 'src/auth2_init.dart'; diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart deleted file mode 100644 index 84f4e6ee8ba8..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -part of gapi_mocks; - -// JS mock of a gapi.auth2, with a successfully identified user -String auth2InitSuccess(GoogleSignInUserData userData) => testIife(''' -${gapi()} - -var mockUser = ${googleUser(userData)}; - -function GapiAuth2() {} -GapiAuth2.prototype.init = function (initOptions) { - /*Leak the initOptions so we can look at them later.*/ - window['gapi2.init.parameters'] = initOptions; - return { - then: (onSuccess, onError) => { - window.setTimeout(() => { - onSuccess(window.gapi.auth2); - }, 30); - }, - currentUser: { - listen: (cb) => { - window.setTimeout(() => { - cb(mockUser); - }, 30); - } - } - } -}; - -GapiAuth2.prototype.getAuthInstance = function () { - return { - signIn: () => { - return new Promise((resolve, reject) => { - window.setTimeout(() => { - resolve(mockUser); - }, 30); - }); - }, - currentUser: { - get: () => mockUser, - }, - } -}; - -window.gapi.auth2 = new GapiAuth2(); -'''); - -String auth2InitError() => testIife(''' -${gapi()} - -function GapiAuth2() {} -GapiAuth2.prototype.init = function (initOptions) { - return { - then: (onSuccess, onError) => { - window.setTimeout(() => { - onError({ - error: 'idpiframe_initialization_failed', - details: 'This error was raised from a test.', - }); - }, 30); - } - } -}; - -window.gapi.auth2 = new GapiAuth2(); -'''); - -String auth2SignInError([String error = 'popup_closed_by_user']) => testIife(''' -${gapi()} - -var mockUser = null; - -function GapiAuth2() {} -GapiAuth2.prototype.init = function (initOptions) { - return { - then: (onSuccess, onError) => { - window.setTimeout(() => { - onSuccess(window.gapi.auth2); - }, 30); - }, - currentUser: { - listen: (cb) => { - window.setTimeout(() => { - cb(mockUser); - }, 30); - } - } - } -}; - -GapiAuth2.prototype.getAuthInstance = function () { - return { - signIn: () => { - return new Promise((resolve, reject) => { - window.setTimeout(() => { - reject({ - error: '$error' - }); - }, 30); - }); - }, - } -}; - -window.gapi.auth2 = new GapiAuth2(); -'''); diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart deleted file mode 100644 index 0e652c647a38..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// The JS mock of the global gapi object -String gapi() => ''' -function Gapi() {}; -Gapi.prototype.load = function (script, cb) { - window.setTimeout(cb, 30); -}; -window.gapi = new Gapi(); -'''; diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart deleted file mode 100644 index e5e6eb262502..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; - -// Creates the JS representation of some user data -String googleUser(GoogleSignInUserData data) => ''' -{ - getBasicProfile: () => { - return { - getName: () => '${data.displayName}', - getEmail: () => '${data.email}', - getId: () => '${data.id}', - getImageUrl: () => '${data.photoUrl}', - }; - }, - getAuthResponse: () => { - return { - id_token: '${data.idToken}', - access_token: 'access_${data.idToken}', - } - }, - getGrantedScopes: () => 'some scope', - grant: () => true, - isSignedIn: () => { - return ${data != null ? 'true' : 'false'}; - }, -} -'''; diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart deleted file mode 100644 index c5aac367c1de..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:google_sign_in_web/src/load_gapi.dart' - show kGapiOnloadCallbackFunctionName; - -// Wraps some JS mock code in an IIFE that ends by calling the onLoad dart callback. -String testIife(String mock) => ''' -(function() { - $mock; - window['$kGapiOnloadCallbackFunctionName'](); -})(); -''' - .replaceAll(RegExp(r'\s{2,}'), ''); diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart deleted file mode 100644 index b9daac44dba8..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in_web/src/js_interop/gapiauth2.dart' as gapi; -import 'package:google_sign_in_web/src/utils.dart'; -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', () { - late FakeGoogleUser fakeUser; - - setUp(() { - fakeUser = FakeGoogleUser(); - }); - - testWidgets('null user -> null response', (WidgetTester tester) async { - expect(gapiUserToPluginUserData(null), isNull); - }); - - testWidgets('not signed-in user -> null response', - (WidgetTester tester) async { - expect(gapiUserToPluginUserData(fakeUser), isNull); - }); - - testWidgets('signed-in, but null profile user -> null response', - (WidgetTester tester) async { - fakeUser.setIsSignedIn(true); - expect(gapiUserToPluginUserData(fakeUser), isNull); - }); - - testWidgets('signed-in, null userId in profile user -> null response', - (WidgetTester tester) async { - 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; - - // ignore: use_setters_to_change_properties - void setIsSignedIn(bool isSignedIn) { - _isSignedIn = isSignedIn; - } - - // ignore: use_setters_to_change_properties - 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/example/integration_test/google_sign_in_web_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart new file mode 100644 index 000000000000..3dcc192e8aaa --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart @@ -0,0 +1,219 @@ +// 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/services.dart' show PlatformException; +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:google_sign_in_web/src/gis_client.dart'; +import 'package:google_sign_in_web/src/people.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart' as mockito; + +import 'google_sign_in_web_test.mocks.dart'; +import 'src/dom.dart'; +import 'src/person.dart'; + +// Mock GisSdkClient so we can simulate any response from the JS side. +@GenerateMocks([], customMocks: >[ + MockSpec(onMissingStub: OnMissingStub.returnDefault), +]) +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Constructor', () { + const String expectedClientId = '3xp3c73d_c113n7_1d'; + + testWidgets('Loads clientId when set in a meta', (_) async { + final GoogleSignInPlugin plugin = GoogleSignInPlugin( + debugOverrideLoader: true, + ); + + expect(plugin.autoDetectedClientId, isNull); + + // Add it to the test page now, and try again + final DomHtmlMetaElement meta = + document.createElement('meta') as DomHtmlMetaElement + ..name = clientIdMetaName + ..content = expectedClientId; + + document.head.appendChild(meta); + + final GoogleSignInPlugin another = GoogleSignInPlugin( + debugOverrideLoader: true, + ); + + expect(another.autoDetectedClientId, expectedClientId); + + // cleanup + meta.remove(); + }); + }); + + group('initWithParams', () { + late GoogleSignInPlugin plugin; + late MockGisSdkClient mockGis; + + setUp(() { + plugin = GoogleSignInPlugin( + debugOverrideLoader: true, + ); + mockGis = MockGisSdkClient(); + }); + + testWidgets('initializes if all is OK', (_) async { + await plugin.initWithParams( + const SignInInitParameters( + clientId: 'some-non-null-client-id', + scopes: ['ok1', 'ok2', 'ok3'], + ), + overrideClient: mockGis, + ); + + expect(plugin.initialized, completes); + }); + + testWidgets('asserts clientId is not null', (_) async { + expect(() async { + await plugin.initWithParams( + const SignInInitParameters(), + overrideClient: mockGis, + ); + }, throwsAssertionError); + }); + + testWidgets('asserts serverClientId must be null', (_) async { + expect(() async { + await plugin.initWithParams( + const SignInInitParameters( + clientId: 'some-non-null-client-id', + serverClientId: 'unexpected-non-null-client-id', + ), + overrideClient: mockGis, + ); + }, throwsAssertionError); + }); + + testWidgets('asserts no scopes have any spaces', (_) async { + expect(() async { + await plugin.initWithParams( + const SignInInitParameters( + clientId: 'some-non-null-client-id', + scopes: ['ok1', 'ok2', 'not ok', 'ok3'], + ), + overrideClient: mockGis, + ); + }, throwsAssertionError); + }); + + testWidgets('must be called for most of the API to work', (_) async { + expect(() async { + await plugin.signInSilently(); + }, throwsStateError); + + expect(() async { + await plugin.signIn(); + }, throwsStateError); + + expect(() async { + await plugin.getTokens(email: ''); + }, throwsStateError); + + expect(() async { + await plugin.signOut(); + }, throwsStateError); + + expect(() async { + await plugin.disconnect(); + }, throwsStateError); + + expect(() async { + await plugin.isSignedIn(); + }, throwsStateError); + + expect(() async { + await plugin.clearAuthCache(token: ''); + }, throwsStateError); + + expect(() async { + await plugin.requestScopes([]); + }, throwsStateError); + }); + }); + + group('(with mocked GIS)', () { + late GoogleSignInPlugin plugin; + late MockGisSdkClient mockGis; + const SignInInitParameters options = SignInInitParameters( + clientId: 'some-non-null-client-id', + scopes: ['ok1', 'ok2', 'ok3'], + ); + + setUp(() { + plugin = GoogleSignInPlugin( + debugOverrideLoader: true, + ); + mockGis = MockGisSdkClient(); + }); + + group('signInSilently', () { + setUp(() { + plugin.initWithParams(options, overrideClient: mockGis); + }); + + testWidgets('always returns null, regardless of GIS response', (_) async { + final GoogleSignInUserData someUser = extractUserData(person)!; + + mockito + .when(mockGis.signInSilently()) + .thenAnswer((_) => Future.value(someUser)); + + expect(plugin.signInSilently(), completion(isNull)); + + mockito + .when(mockGis.signInSilently()) + .thenAnswer((_) => Future.value()); + + expect(plugin.signInSilently(), completion(isNull)); + }); + }); + + group('signIn', () { + setUp(() { + plugin.initWithParams(options, overrideClient: mockGis); + }); + + testWidgets('returns the signed-in user', (_) async { + final GoogleSignInUserData someUser = extractUserData(person)!; + + mockito + .when(mockGis.signIn()) + .thenAnswer((_) => Future.value(someUser)); + + expect(await plugin.signIn(), someUser); + }); + + testWidgets('returns null if no user is signed in', (_) async { + mockito + .when(mockGis.signIn()) + .thenAnswer((_) => Future.value()); + + expect(await plugin.signIn(), isNull); + }); + + testWidgets('converts inner errors to PlatformException', (_) async { + mockito.when(mockGis.signIn()).thenThrow('popup_closed'); + + try { + await plugin.signIn(); + fail('signIn should have thrown an exception'); + } catch (exception) { + expect(exception, isA()); + expect((exception as PlatformException).code, 'popup_closed'); + } + }); + }); + }); +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart new file mode 100644 index 000000000000..b60dac9d4b95 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart @@ -0,0 +1,125 @@ +// Mocks generated by Mockito 5.3.2 from annotations +// in google_sign_in_web_integration_tests/integration_test/google_sign_in_web_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i4; + +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart' + as _i2; +import 'package:google_sign_in_web/src/gis_client.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeGoogleSignInTokenData_0 extends _i1.SmartFake + implements _i2.GoogleSignInTokenData { + _FakeGoogleSignInTokenData_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [GisSdkClient]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient { + @override + _i4.Future<_i2.GoogleSignInUserData?> signInSilently() => (super.noSuchMethod( + Invocation.method( + #signInSilently, + [], + ), + returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(), + returnValueForMissingStub: + _i4.Future<_i2.GoogleSignInUserData?>.value(), + ) as _i4.Future<_i2.GoogleSignInUserData?>); + @override + _i4.Future<_i2.GoogleSignInUserData?> signIn() => (super.noSuchMethod( + Invocation.method( + #signIn, + [], + ), + returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(), + returnValueForMissingStub: + _i4.Future<_i2.GoogleSignInUserData?>.value(), + ) as _i4.Future<_i2.GoogleSignInUserData?>); + @override + _i2.GoogleSignInTokenData getTokens() => (super.noSuchMethod( + Invocation.method( + #getTokens, + [], + ), + returnValue: _FakeGoogleSignInTokenData_0( + this, + Invocation.method( + #getTokens, + [], + ), + ), + returnValueForMissingStub: _FakeGoogleSignInTokenData_0( + this, + Invocation.method( + #getTokens, + [], + ), + ), + ) as _i2.GoogleSignInTokenData); + @override + _i4.Future signOut() => (super.noSuchMethod( + Invocation.method( + #signOut, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future disconnect() => (super.noSuchMethod( + Invocation.method( + #disconnect, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future isSignedIn() => (super.noSuchMethod( + Invocation.method( + #isSignedIn, + [], + ), + returnValue: _i4.Future.value(false), + returnValueForMissingStub: _i4.Future.value(false), + ) as _i4.Future); + @override + _i4.Future clearAuthCache() => (super.noSuchMethod( + Invocation.method( + #clearAuthCache, + [], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); + @override + _i4.Future requestScopes(List? scopes) => (super.noSuchMethod( + Invocation.method( + #requestScopes, + [scopes], + ), + returnValue: _i4.Future.value(false), + returnValueForMissingStub: _i4.Future.value(false), + ) as _i4.Future); +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart new file mode 100644 index 000000000000..e81ccb6e95b5 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart @@ -0,0 +1,132 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_identity_services_web/oauth2.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:google_sign_in_web/src/people.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart' as http_test; +import 'package:integration_test/integration_test.dart'; + +import 'src/jsify_as.dart'; +import 'src/person.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('requestUserData', () { + const String expectedAccessToken = '3xp3c73d_4cc355_70k3n'; + + final TokenResponse fakeToken = jsifyAs({ + 'token_type': 'Bearer', + 'access_token': expectedAccessToken, + }); + + testWidgets('happy case', (_) async { + final Completer accessTokenCompleter = Completer(); + + final http.Client mockClient = http_test.MockClient( + (http.Request request) async { + accessTokenCompleter.complete(request.headers['Authorization']); + + return http.Response( + jsonEncode(person), + 200, + headers: {'content-type': 'application/json'}, + ); + }, + ); + + final GoogleSignInUserData? user = await requestUserData( + fakeToken, + overrideClient: mockClient, + ); + + expect(user, isNotNull); + expect(user!.email, expectedPersonEmail); + expect(user.id, expectedPersonId); + expect(user.displayName, expectedPersonName); + expect(user.photoUrl, expectedPersonPhoto); + expect(user.idToken, isNull); + expect( + accessTokenCompleter.future, + completion('Bearer $expectedAccessToken'), + ); + }); + + testWidgets('Unauthorized request - throws exception', (_) async { + final http.Client mockClient = http_test.MockClient( + (http.Request request) async { + return http.Response( + 'Unauthorized', + 403, + ); + }, + ); + + expect(() async { + await requestUserData( + fakeToken, + overrideClient: mockClient, + ); + }, throwsA(isA())); + }); + }); + + group('extractUserData', () { + testWidgets('happy case', (_) async { + final GoogleSignInUserData? user = extractUserData(person); + + expect(user, isNotNull); + expect(user!.email, expectedPersonEmail); + expect(user.id, expectedPersonId); + expect(user.displayName, expectedPersonName); + expect(user.photoUrl, expectedPersonPhoto); + expect(user.idToken, isNull); + }); + + testWidgets('no name/photo - keeps going', (_) async { + final Map personWithoutSomeData = + mapWithoutKeys(person, { + 'names', + 'photos', + }); + + final GoogleSignInUserData? user = extractUserData(personWithoutSomeData); + + expect(user, isNotNull); + expect(user!.email, expectedPersonEmail); + expect(user.id, expectedPersonId); + expect(user.displayName, isNull); + expect(user.photoUrl, isNull); + expect(user.idToken, isNull); + }); + + testWidgets('no userId - throws assertion error', (_) async { + final Map personWithoutId = + mapWithoutKeys(person, { + 'resourceName', + }); + + expect(() { + extractUserData(personWithoutId); + }, throwsAssertionError); + }); + + testWidgets('no email - throws assertion error', (_) async { + final Map personWithoutEmail = + mapWithoutKeys(person, { + 'emailAddresses', + }); + + expect(() { + extractUserData(personWithoutEmail); + }, throwsAssertionError); + }); + }); +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/dom.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/dom.dart new file mode 100644 index 000000000000..f7d3152a7e64 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/dom.dart @@ -0,0 +1,59 @@ +// 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. + +/* +// DOM shim. This file contains everything we need from the DOM API written as +// @staticInterop, so we don't need dart:html +// https://developer.mozilla.org/en-US/docs/Web/API/ +// +// (To be replaced by `package:web`) +*/ + +import 'package:js/js.dart'; + +/// Document interface +@JS() +@staticInterop +abstract class DomHtmlDocument {} + +/// Some methods of document +extension DomHtmlDocumentExtension on DomHtmlDocument { + /// document.head + external DomHtmlElement get head; + + /// document.createElement + external DomHtmlElement createElement(String tagName); +} + +/// An instance of an HTMLElement +@JS() +@staticInterop +abstract class DomHtmlElement {} + +/// (Some) methods of HtmlElement +extension DomHtmlElementExtension on DomHtmlElement { + /// Node.appendChild + external DomHtmlElement appendChild(DomHtmlElement child); + + /// Element.remove + external void remove(); +} + +/// An instance of an HTMLMetaElement +@JS() +@staticInterop +abstract class DomHtmlMetaElement extends DomHtmlElement {} + +/// Some methods exclusive of Script elements +extension DomHtmlMetaElementExtension on DomHtmlMetaElement { + external set name(String name); + external set content(String content); +} + +// Getters + +/// window.document +@JS() +@staticInterop +external DomHtmlDocument get document; diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jsify_as.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jsify_as.dart new file mode 100644 index 000000000000..82547b284fe0 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jsify_as.dart @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:js/js_util.dart' as js_util; + +/// Converts a [data] object into a JS Object of type `T`. +T jsifyAs(Map data) { + return js_util.jsify(data) as T; +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jwt_examples.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jwt_examples.dart new file mode 100644 index 000000000000..72841c5165ee --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jwt_examples.dart @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:google_identity_services_web/id.dart'; + +import 'jsify_as.dart'; + +/// A CredentialResponse with null `credential`. +final CredentialResponse nullCredential = + jsifyAs({ + 'credential': null, +}); + +/// A CredentialResponse wrapping a known good JWT Token as its `credential`. +final CredentialResponse goodCredential = + jsifyAs({ + 'credential': goodJwtToken, +}); + +/// A JWT token with predefined values. +/// +/// 'email': 'adultman@example.com', +/// 'sub': '123456', +/// 'name': 'Vincent Adultman', +/// 'picture': 'https://thispersondoesnotexist.com/image?x=.jpg', +/// +/// Signed with HS256 and the private key: 'symmetric-encryption-is-weak' +const String goodJwtToken = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.$goodPayload.lqzULA_U3YzEl_-fL7YLU-kFXmdD2ttJLTv-UslaNQ4'; + +/// The payload of a JWT token that contains predefined values. +/// +/// 'email': 'adultman@example.com', +/// 'sub': '123456', +/// 'name': 'Vincent Adultman', +/// 'picture': 'https://thispersondoesnotexist.com/image?x=.jpg', +const String goodPayload = + 'eyJlbWFpbCI6ImFkdWx0bWFuQGV4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2IiwibmFtZSI6IlZpbmNlbnQgQWR1bHRtYW4iLCJwaWN0dXJlIjoiaHR0cHM6Ly90aGlzcGVyc29uZG9lc25vdGV4aXN0LmNvbS9pbWFnZT94PS5qcGcifQ'; + +// More encrypted JWT Tokens may be created on https://jwt.io. +// +// First, decode the `goodJwtToken` above, modify to your heart's +// content, and add a new credential here. +// +// (New tokens can also be created with `package:jose` and `dart:convert`.) diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart new file mode 100644 index 000000000000..2525596eabe9 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart @@ -0,0 +1,66 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +const String expectedPersonId = '1234567890'; +const String expectedPersonName = 'Vincent Adultman'; +const String expectedPersonEmail = 'adultman@example.com'; +const String expectedPersonPhoto = + 'https://thispersondoesnotexist.com/image?x=.jpg'; + +/// A subset of https://developers.google.com/people/api/rest/v1/people#Person. +final Map person = { + 'resourceName': 'people/$expectedPersonId', + 'emailAddresses': [ + { + 'metadata': { + 'primary': false, + }, + 'value': 'bad@example.com', + }, + { + 'metadata': {}, + 'value': 'nope@example.com', + }, + { + 'metadata': { + 'primary': true, + }, + 'value': expectedPersonEmail, + }, + ], + 'names': [ + { + 'metadata': { + 'primary': true, + }, + 'displayName': expectedPersonName, + }, + { + 'metadata': { + 'primary': false, + }, + 'displayName': 'Fakey McFakeface', + }, + ], + 'photos': [ + { + 'metadata': { + 'primary': true, + }, + 'url': expectedPersonPhoto, + }, + ], +}; + +/// Returns a copy of [map] without the [keysToRemove]. +T mapWithoutKeys>( + T map, + Set keysToRemove, +) { + return Map.fromEntries( + map.entries.where((MapEntry entry) { + return !keysToRemove.contains(entry.key); + }), + ) as T; +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart deleted file mode 100644 index 56aa61df136e..000000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; - -String toBase64Url(String contents) { - // Open the file - return 'data:text/javascript;base64,${base64.encode(utf8.encode(contents))}'; -} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart new file mode 100644 index 000000000000..82701e587be1 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart @@ -0,0 +1,173 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_identity_services_web/id.dart'; +import 'package:google_identity_services_web/oauth2.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:google_sign_in_web/src/utils.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'src/jsify_as.dart'; +import 'src/jwt_examples.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('gisResponsesToTokenData', () { + testWidgets('null objects -> no problem', (_) async { + final GoogleSignInTokenData tokens = gisResponsesToTokenData(null, null); + + expect(tokens.accessToken, isNull); + expect(tokens.idToken, isNull); + expect(tokens.serverAuthCode, isNull); + }); + + testWidgets('non-null objects are correctly used', (_) async { + const String expectedIdToken = 'some-value-for-testing'; + const String expectedAccessToken = 'another-value-for-testing'; + + final CredentialResponse credential = + jsifyAs({ + 'credential': expectedIdToken, + }); + final TokenResponse token = jsifyAs({ + 'access_token': expectedAccessToken, + }); + final GoogleSignInTokenData tokens = + gisResponsesToTokenData(credential, token); + + expect(tokens.accessToken, expectedAccessToken); + expect(tokens.idToken, expectedIdToken); + expect(tokens.serverAuthCode, isNull); + }); + }); + + group('gisResponsesToUserData', () { + testWidgets('happy case', (_) async { + final GoogleSignInUserData data = gisResponsesToUserData(goodCredential)!; + + expect(data.displayName, 'Vincent Adultman'); + expect(data.id, '123456'); + expect(data.email, 'adultman@example.com'); + expect(data.photoUrl, 'https://thispersondoesnotexist.com/image?x=.jpg'); + expect(data.idToken, goodJwtToken); + }); + + testWidgets('null response -> null', (_) async { + expect(gisResponsesToUserData(null), isNull); + }); + + testWidgets('null response.credential -> null', (_) async { + expect(gisResponsesToUserData(nullCredential), isNull); + }); + + testWidgets('invalid payload -> null', (_) async { + final CredentialResponse response = + jsifyAs({ + 'credential': 'some-bogus.thing-that-is-not.valid-jwt', + }); + expect(gisResponsesToUserData(response), isNull); + }); + }); + + group('getJwtTokenPayload', () { + testWidgets('happy case -> data', (_) async { + final Map? data = getJwtTokenPayload(goodJwtToken); + + expect(data, isNotNull); + expect(data, containsPair('name', 'Vincent Adultman')); + expect(data, containsPair('email', 'adultman@example.com')); + expect(data, containsPair('sub', '123456')); + expect( + data, + containsPair( + 'picture', + 'https://thispersondoesnotexist.com/image?x=.jpg', + )); + }); + + testWidgets('null Token -> null', (_) async { + final Map? data = getJwtTokenPayload(null); + + expect(data, isNull); + }); + + testWidgets('Token not matching the format -> null', (_) async { + final Map? data = getJwtTokenPayload('1234.4321'); + + expect(data, isNull); + }); + + testWidgets('Bad token that matches the format -> null', (_) async { + final Map? data = getJwtTokenPayload('1234.abcd.4321'); + + expect(data, isNull); + }); + }); + + group('decodeJwtPayload', () { + testWidgets('Good payload -> data', (_) async { + final Map? data = decodeJwtPayload(goodPayload); + + expect(data, isNotNull); + expect(data, containsPair('name', 'Vincent Adultman')); + expect(data, containsPair('email', 'adultman@example.com')); + expect(data, containsPair('sub', '123456')); + expect( + data, + containsPair( + 'picture', + 'https://thispersondoesnotexist.com/image?x=.jpg', + )); + }); + + testWidgets('Proper JSON payload -> data', (_) async { + final String payload = base64.encode(utf8.encode('{"properJson": true}')); + + final Map? data = decodeJwtPayload(payload); + + expect(data, isNotNull); + expect(data, containsPair('properJson', true)); + }); + + testWidgets('Not-normalized base-64 payload -> data', (_) async { + // This is the payload generated by the "Proper JSON payload" test, but + // we remove the leading "=" symbols so it's length is not a multiple of 4 + // anymore! + final String payload = 'eyJwcm9wZXJKc29uIjogdHJ1ZX0='.replaceAll('=', ''); + + final Map? data = decodeJwtPayload(payload); + + expect(data, isNotNull); + expect(data, containsPair('properJson', true)); + }); + + testWidgets('Invalid JSON payload -> null', (_) async { + final String payload = base64.encode(utf8.encode('{properJson: false}')); + + final Map? data = decodeJwtPayload(payload); + + expect(data, isNull); + }); + + testWidgets('Non JSON payload -> null', (_) async { + final String payload = base64.encode(utf8.encode('not-json')); + + final Map? data = decodeJwtPayload(payload); + + expect(data, isNull); + }); + + testWidgets('Non base-64 payload -> null', (_) async { + const String payload = 'not-base-64-at-all'; + + final Map? data = decodeJwtPayload(payload); + + expect(data, isNull); + }); + }); +} diff --git a/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml index e5abdacf944d..c73953374696 100644 --- a/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: @@ -12,12 +12,15 @@ dependencies: path: ../ dev_dependencies: + build_runner: ^2.1.1 flutter_driver: sdk: flutter flutter_test: sdk: flutter + google_identity_services_web: ^0.2.0 google_sign_in_platform_interface: ^2.2.0 http: ^0.13.0 integration_test: sdk: flutter js: ^0.6.3 + mockito: ^5.3.2 diff --git a/.ci/scripts/plugin_tools_tests.sh b/packages/google_sign_in/google_sign_in_web/example/regen_mocks.sh old mode 100644 new mode 100755 similarity index 56% rename from .ci/scripts/plugin_tools_tests.sh rename to packages/google_sign_in/google_sign_in_web/example/regen_mocks.sh index 96eec4349f08..78bcdc0f9e28 --- a/.ci/scripts/plugin_tools_tests.sh +++ b/packages/google_sign_in/google_sign_in_web/example/regen_mocks.sh @@ -1,7 +1,10 @@ -#!/bin/bash +#!/usr/bin/bash # Copyright 2013 The Flutter Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -cd script/tool -dart pub run test +flutter pub get + +echo "(Re)generating mocks." + +flutter pub run build_runner build --delete-conflicting-outputs diff --git a/packages/google_sign_in/google_sign_in_web/example/run_test.sh b/packages/google_sign_in/google_sign_in_web/example/run_test.sh index 28877dce8d6e..fcac5f600acb 100755 --- a/packages/google_sign_in/google_sign_in_web/example/run_test.sh +++ b/packages/google_sign_in/google_sign_in_web/example/run_test.sh @@ -6,9 +6,11 @@ if pgrep -lf chromedriver > /dev/null; then echo "chromedriver is running." + ./regen_mocks.sh + 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.dart --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 @@ -17,7 +19,6 @@ if pgrep -lf chromedriver > /dev/null; then 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/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 5d75c0da0c4f..827b17ca5b44 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 @@ -5,23 +5,22 @@ import 'dart:async'; import 'dart:html' as html; -import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting, kDebugMode; +import 'package:flutter/services.dart' show PlatformException; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:google_identity_services_web/loader.dart' as loader; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'package:js/js.dart'; -import 'src/js_interop/gapiauth2.dart' as auth2; -import 'src/load_gapi.dart' as gapi; -import 'src/utils.dart' show gapiUserToPluginUserData; +import 'src/gis_client.dart'; -const String _kClientIdMetaSelector = 'meta[name=google-signin-client_id]'; -const String _kClientIdAttributeName = 'content'; +/// The `name` of the meta-tag to define a ClientID in HTML. +const String clientIdMetaName = 'google-signin-client_id'; -/// This is only exposed for testing. It shouldn't be accessed by users of the -/// plugin as it could break at any point. -@visibleForTesting -String gapiUrl = 'https://apis.google.com/js/platform.js'; +/// The selector used to find the meta-tag that defines a ClientID in HTML. +const String clientIdMetaSelector = 'meta[name=$clientIdMetaName]'; + +/// The attribute name that stores the Client ID in the meta-tag that defines a Client ID in HTML. +const String clientIdAttributeName = 'content'; /// Implementation of the google_sign_in plugin for Web. class GoogleSignInPlugin extends GoogleSignInPlatform { @@ -29,18 +28,24 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { /// background. /// /// The plugin is completely initialized when [initialized] completed. - GoogleSignInPlugin() { - _autoDetectedClientId = html - .querySelector(_kClientIdMetaSelector) - ?.getAttribute(_kClientIdAttributeName); - - _isGapiInitialized = gapi.inject(gapiUrl).then((_) => gapi.init()); + GoogleSignInPlugin({@visibleForTesting bool debugOverrideLoader = false}) { + autoDetectedClientId = html + .querySelector(clientIdMetaSelector) + ?.getAttribute(clientIdAttributeName); + + if (debugOverrideLoader) { + _jsSdkLoadedFuture = Future.value(true); + } else { + _jsSdkLoadedFuture = loader.loadWebSdk(); + } } - late Future _isGapiInitialized; - late Future _isAuthInitialized; + late Future _jsSdkLoadedFuture; bool _isInitCalled = false; + // The instance of [GisSdkClient] backing the plugin. + late GisSdkClient _gisClient; + // This method throws if init or initWithParams hasn't been called at some // point in the past. It is used by the [initialized] getter to ensure that // users can't await on a Future that will never resolve. @@ -53,14 +58,16 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { } } - /// A future that resolves when both GAPI and Auth2 have been correctly initialized. + /// A future that resolves when the SDK has been correctly loaded. @visibleForTesting Future get initialized { _assertIsInitCalled(); - return Future.wait(>[_isGapiInitialized, _isAuthInitialized]); + return _jsSdkLoadedFuture; } - String? _autoDetectedClientId; + /// Stores the client ID if it was set in a meta-tag of the page. + @visibleForTesting + late String? autoDetectedClientId; /// Factory method that initializes the plugin with [GoogleSignInPlatform]. static void registerWith(Registrar registrar) { @@ -83,8 +90,11 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { } @override - Future initWithParams(SignInInitParameters params) async { - final String? appClientId = params.clientId ?? _autoDetectedClientId; + Future initWithParams( + SignInInitParameters params, { + @visibleForTesting GisSdkClient? overrideClient, + }) async { + final String? appClientId = params.clientId ?? autoDetectedClientId; assert( appClientId != null, 'ClientID not set. Either set it on a ' @@ -100,141 +110,95 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { 'Check https://developers.google.com/identity/protocols/googlescopes ' 'for a list of valid OAuth 2.0 scopes.'); - await _isGapiInitialized; + await _jsSdkLoadedFuture; - final auth2.GoogleAuth auth = auth2.init(auth2.ClientConfig( - hosted_domain: params.hostedDomain, - // The js lib wants a space-separated list of values - scope: params.scopes.join(' '), - client_id: appClientId!, - plugin_name: 'dart-google_sign_in_web', - )); + _gisClient = overrideClient ?? + GisSdkClient( + clientId: appClientId!, + hostedDomain: params.hostedDomain, + initialScopes: List.from(params.scopes), + loggingEnabled: kDebugMode, + ); - final Completer isAuthInitialized = Completer(); - _isAuthInitialized = isAuthInitialized.future; _isInitCalled = true; - - auth.then(allowInterop((auth2.GoogleAuth initializedAuth) { - // onSuccess - - // TODO(ditman): https://github.com/flutter/flutter/issues/48528 - // This plugin doesn't notify the app of external changes to the - // state of the authentication, i.e: if you logout elsewhere... - - isAuthInitialized.complete(); - }), allowInterop((auth2.GoogleAuthInitFailureError reason) { - // onError - isAuthInitialized.completeError(PlatformException( - code: reason.error, - message: reason.details, - details: - 'https://developers.google.com/identity/sign-in/web/reference#error_codes', - )); - })); - - return _isAuthInitialized; } @override Future signInSilently() async { await initialized; - return gapiUserToPluginUserData( - auth2.getAuthInstance()?.currentUser?.get()); + // Since the new GIS SDK does *not* perform authorization at the same time as + // authentication (and every one of our users expects that), we need to tell + // the plugin that this failed regardless of the actual result. + // + // However, if this succeeds, we'll save a People API request later. + return _gisClient.signInSilently().then((_) => null); } @override Future signIn() async { await initialized; + + // This method mainly does oauth2 authorization, which happens to also do + // authentication if needed. However, the authentication information is not + // returned anymore. + // + // This method will synthesize authentication information from the People API + // if needed (or use the last identity seen from signInSilently). try { - return gapiUserToPluginUserData(await auth2.getAuthInstance()?.signIn()); - } on auth2.GoogleAuthSignInError catch (reason) { + return _gisClient.signIn(); + } catch (reason) { throw PlatformException( - code: reason.error, - message: 'Exception raised from GoogleAuth.signIn()', + code: reason.toString(), + message: 'Exception raised from signIn', details: - 'https://developers.google.com/identity/sign-in/web/reference#error_codes_2', + 'https://developers.google.com/identity/oauth2/web/guides/error', ); } } @override - Future getTokens( - {required String email, bool? shouldRecoverAuth}) async { + Future getTokens({ + required String email, + bool? shouldRecoverAuth, + }) async { await initialized; - final auth2.GoogleUser? currentUser = - auth2.getAuthInstance()?.currentUser?.get(); - final auth2.AuthResponse? response = currentUser?.getAuthResponse(); - - return GoogleSignInTokenData( - idToken: response?.id_token, accessToken: response?.access_token); + return _gisClient.getTokens(); } @override Future signOut() async { await initialized; - return auth2.getAuthInstance()?.signOut(); + _gisClient.signOut(); } @override Future disconnect() async { await initialized; - final auth2.GoogleUser? currentUser = - auth2.getAuthInstance()?.currentUser?.get(); - - if (currentUser == null) { - return; - } - - return currentUser.disconnect(); + _gisClient.disconnect(); } @override Future isSignedIn() async { await initialized; - final auth2.GoogleUser? currentUser = - auth2.getAuthInstance()?.currentUser?.get(); - - if (currentUser == null) { - return false; - } - - return currentUser.isSignedIn(); + return _gisClient.isSignedIn(); } @override Future clearAuthCache({required String token}) async { await initialized; - return auth2.getAuthInstance()?.disconnect(); + _gisClient.clearAuthCache(); } @override Future requestScopes(List scopes) async { await initialized; - final auth2.GoogleUser? currentUser = - auth2.getAuthInstance()?.currentUser?.get(); - - if (currentUser == null) { - return false; - } - - final String grantedScopes = currentUser.getGrantedScopes() ?? ''; - final Iterable missingScopes = - scopes.where((String scope) => !grantedScopes.contains(scope)); - - if (missingScopes.isEmpty) { - return true; - } - - final Object? response = await currentUser - .grant(auth2.SigninOptions(scope: missingScopes.join(' '))); - - return response != null; + return _gisClient.requestScopes(scopes); } } diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart b/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart new file mode 100644 index 000000000000..3815322e6900 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart @@ -0,0 +1,310 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'dart:async'; + +// TODO(dit): Split `id` and `oauth2` "services" for mocking. https://github.com/flutter/flutter/issues/120657 +import 'package:google_identity_services_web/id.dart'; +import 'package:google_identity_services_web/oauth2.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +// ignore: unnecessary_import +import 'package:js/js.dart'; +import 'package:js/js_util.dart'; + +import 'people.dart' as people; +import 'utils.dart' as utils; + +/// A client to hide (most) of the interaction with the GIS SDK from the plugin. +/// +/// (Overridable for testing) +class GisSdkClient { + /// Create a GisSdkClient object. + GisSdkClient({ + required List initialScopes, + required String clientId, + bool loggingEnabled = false, + String? hostedDomain, + }) : _initialScopes = initialScopes { + if (loggingEnabled) { + id.setLogLevel('debug'); + } + // Configure the Stream objects that are going to be used by the clients. + _configureStreams(); + + // Initialize the SDK clients we need. + _initializeIdClient( + clientId, + onResponse: _onCredentialResponse, + ); + + _tokenClient = _initializeTokenClient( + clientId, + hostedDomain: hostedDomain, + onResponse: _onTokenResponse, + onError: _onTokenError, + ); + } + + // Configure the credential (authentication) and token (authorization) response streams. + void _configureStreams() { + _tokenResponses = StreamController.broadcast(); + _credentialResponses = StreamController.broadcast(); + _tokenResponses.stream.listen((TokenResponse response) { + _lastTokenResponse = response; + }, onError: (Object error) { + _lastTokenResponse = null; + }); + _credentialResponses.stream.listen((CredentialResponse response) { + _lastCredentialResponse = response; + }, onError: (Object error) { + _lastCredentialResponse = null; + }); + } + + // Initializes the `id` SDK for the silent-sign in (authentication) client. + void _initializeIdClient( + String clientId, { + required CallbackFn onResponse, + }) { + // Initialize `id` for the silent-sign in code. + final IdConfiguration idConfig = IdConfiguration( + client_id: clientId, + callback: allowInterop(onResponse), + cancel_on_tap_outside: false, + auto_select: true, // Attempt to sign-in silently. + ); + id.initialize(idConfig); + } + + // Handle a "normal" credential (authentication) response. + // + // (Normal doesn't mean successful, this might contain `error` information.) + void _onCredentialResponse(CredentialResponse response) { + if (response.error != null) { + _credentialResponses.addError(response.error!); + } else { + _credentialResponses.add(response); + } + } + + // Creates a `oauth2.TokenClient` used for authorization (scope) requests. + TokenClient _initializeTokenClient( + String clientId, { + String? hostedDomain, + required TokenClientCallbackFn onResponse, + required ErrorCallbackFn onError, + }) { + // Create a Token Client for authorization calls. + final TokenClientConfig tokenConfig = TokenClientConfig( + client_id: clientId, + hosted_domain: hostedDomain, + callback: allowInterop(_onTokenResponse), + error_callback: allowInterop(_onTokenError), + // `scope` will be modified by the `signIn` method, in case we need to + // backfill user Profile info. + scope: ' ', + ); + return oauth2.initTokenClient(tokenConfig); + } + + // Handle a "normal" token (authorization) response. + // + // (Normal doesn't mean successful, this might contain `error` information.) + void _onTokenResponse(TokenResponse response) { + if (response.error != null) { + _tokenResponses.addError(response.error!); + } else { + _tokenResponses.add(response); + } + } + + // Handle a "not-directly-related-to-authorization" error. + // + // Token clients have an additional `error_callback` for miscellaneous + // errors, like "popup couldn't open" or "popup closed by user". + void _onTokenError(Object? error) { + // This is handled in a funky (js_interop) way because of: + // https://github.com/dart-lang/sdk/issues/50899 + _tokenResponses.addError(getProperty(error!, 'type')); + } + + /// Attempts to sign-in the user using the OneTap UX flow. + /// + /// If the user consents, to OneTap, the [GoogleSignInUserData] will be + /// generated from a proper [CredentialResponse], which contains `idToken`. + /// Else, it'll be synthesized by a request to the People API later, and the + /// `idToken` will be null. + Future signInSilently() async { + final Completer userDataCompleter = + Completer(); + + // Ask the SDK to render the OneClick sign-in. + // + // And also handle its "moments". + id.prompt(allowInterop((PromptMomentNotification moment) { + _onPromptMoment(moment, userDataCompleter); + })); + + return userDataCompleter.future; + } + + // Handles "prompt moments" of the OneClick card UI. + // + // See: https://developers.google.com/identity/gsi/web/guides/receive-notifications-prompt-ui-status + Future _onPromptMoment( + PromptMomentNotification moment, + Completer completer, + ) async { + if (completer.isCompleted) { + return; // Skip once the moment has been handled. + } + + if (moment.isDismissedMoment() && + moment.getDismissedReason() == + MomentDismissedReason.credential_returned) { + // Kick this part of the handler to the bottom of the JS event queue, so + // the _credentialResponses stream has time to propagate its last value, + // and we can use _lastCredentialResponse. + return Future.delayed(Duration.zero, () { + completer + .complete(utils.gisResponsesToUserData(_lastCredentialResponse)); + }); + } + + // In any other 'failed' moments, return null and add an error to the stream. + if (moment.isNotDisplayed() || + moment.isSkippedMoment() || + moment.isDismissedMoment()) { + final String reason = moment.getNotDisplayedReason()?.toString() ?? + moment.getSkippedReason()?.toString() ?? + moment.getDismissedReason()?.toString() ?? + 'unknown_error'; + + _credentialResponses.addError(reason); + completer.complete(null); + } + } + + /// Starts an oauth2 "implicit" flow to authorize requests. + /// + /// The new GIS SDK does not return user authentication from this flow, so: + /// * If [_lastCredentialResponse] is **not** null (the user has successfully + /// `signInSilently`), we return that after this method completes. + /// * If [_lastCredentialResponse] is null, we add [people.scopes] to the + /// [_initialScopes], so we can retrieve User Profile information back + /// from the People API (without idToken). See [people.requestUserData]. + Future signIn() async { + // If we already know the user, use their `email` as a `hint`, so they don't + // have to pick their user again in the Authorization popup. + final GoogleSignInUserData? knownUser = + utils.gisResponsesToUserData(_lastCredentialResponse); + // This toggles a popup, so `signIn` *must* be called with + // user activation. + _tokenClient.requestAccessToken(OverridableTokenClientConfig( + prompt: knownUser == null ? 'select_account' : '', + hint: knownUser?.email, + scope: [ + ..._initialScopes, + // If the user hasn't gone through the auth process, + // the plugin will attempt to `requestUserData` after, + // so we need extra scopes to retrieve that info. + if (_lastCredentialResponse == null) ...people.scopes, + ].join(' '), + )); + + await _tokenResponses.stream.first; + + return _computeUserDataForLastToken(); + } + + // This function returns the currently signed-in [GoogleSignInUserData]. + // + // It'll do a request to the People API (if needed). + Future _computeUserDataForLastToken() async { + // If the user hasn't authenticated, request their basic profile info + // from the People API. + // + // This synthetic response will *not* contain an `idToken` field. + if (_lastCredentialResponse == null && _requestedUserData == null) { + assert(_lastTokenResponse != null); + _requestedUserData = await people.requestUserData(_lastTokenResponse!); + } + // Complete user data either with the _lastCredentialResponse seen, + // or the synthetic _requestedUserData from above. + return utils.gisResponsesToUserData(_lastCredentialResponse) ?? + _requestedUserData; + } + + /// Returns a [GoogleSignInTokenData] from the latest seen responses. + GoogleSignInTokenData getTokens() { + return utils.gisResponsesToTokenData( + _lastCredentialResponse, + _lastTokenResponse, + ); + } + + /// Revokes the current authentication. + Future signOut() async { + clearAuthCache(); + id.disableAutoSelect(); + } + + /// Revokes the current authorization and authentication. + Future disconnect() async { + if (_lastTokenResponse != null) { + oauth2.revoke(_lastTokenResponse!.access_token); + } + signOut(); + } + + /// Returns true if the client has recognized this user before. + Future isSignedIn() async { + return _lastCredentialResponse != null || _requestedUserData != null; + } + + /// Clears all the cached results from authentication and authorization. + Future clearAuthCache() async { + _lastCredentialResponse = null; + _lastTokenResponse = null; + _requestedUserData = null; + } + + /// Requests the list of [scopes] passed in to the client. + /// + /// Keeps the previously granted scopes. + Future requestScopes(List scopes) async { + _tokenClient.requestAccessToken(OverridableTokenClientConfig( + scope: scopes.join(' '), + include_granted_scopes: true, + )); + + await _tokenResponses.stream.first; + + return oauth2.hasGrantedAllScopes(_lastTokenResponse!, scopes); + } + + // The scopes initially requested by the developer. + // + // We store this because we might need to add more at `signIn`. If the user + // doesn't `silentSignIn`, we expand this list to consult the People API to + // return some basic Authentication information. + final List _initialScopes; + + // The Google Identity Services client for oauth requests. + late TokenClient _tokenClient; + + // Streams of credential and token responses. + late StreamController _credentialResponses; + late StreamController _tokenResponses; + + // The last-seen credential and token responses + CredentialResponse? _lastCredentialResponse; + TokenResponse? _lastTokenResponse; + + // If the user *authenticates* (signs in) through oauth2, the SDK doesn't return + // identity information anymore, so we synthesize it by calling the PeopleAPI + // (if needed) + // + // (This is a synthetic _lastCredentialResponse) + GoogleSignInUserData? _requestedUserData; +} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/js_interop/gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/js_interop/gapi.dart deleted file mode 100644 index 3be4b2d77b66..000000000000 --- a/packages/google_sign_in/google_sign_in_web/lib/src/js_interop/gapi.dart +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// 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 - -// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi - -// ignore_for_file: public_member_api_docs, -// * public_member_api_docs originally undocumented because the file was -// autogenerated. - -@JS() -library gapi; - -import 'package:js/js.dart'; - -// Module gapi -typedef LoadCallback = void Function( - [dynamic args1, - dynamic args2, - dynamic args3, - dynamic args4, - dynamic args5]); - -@anonymous -@JS() -abstract class LoadConfig { - external factory LoadConfig( - {LoadCallback callback, - Function? onerror, - num? timeout, - Function? ontimeout}); - 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); -} - -/*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') -external void load( - String apiName, dynamic /*LoadConfig|LoadCallback*/ callback); -// End module gapi - -// Manually removed gapi.auth and gapi.client, unused by this plugin. diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/js_interop/gapiauth2.dart b/packages/google_sign_in/google_sign_in_web/lib/src/js_interop/gapiauth2.dart deleted file mode 100644 index 35a2d08e74b6..000000000000 --- a/packages/google_sign_in/google_sign_in_web/lib/src/js_interop/gapiauth2.dart +++ /dev/null @@ -1,497 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// 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 -/// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -/// TypeScript Version: 2.3 - -/// - -// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi.auth2 - -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, -// * public_member_api_docs originally undocumented because the file was -// autogenerated. -// * non_constant_identifier_names required to be able to use the same parameter -// names as the underlying library. - -@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 String get details; - external set details(String? value); -} - -@anonymous -@JS() -class GoogleAuthSignInError { - external String get error; - 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') -class GoogleAuth { - external IsSignedIn get isSignedIn; - external set isSignedIn(IsSignedIn 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. - external dynamic then(dynamic Function(GoogleAuth googleAuth) onInit, - [dynamic Function(GoogleAuthInitFailureError reason) onFailure]); - - /// Signs out all accounts from the application. - external dynamic signOut(); - - /// Revokes all of the scopes that the user granted. - external dynamic disconnect(); - - /// Attaches the sign-in flow to the specified container's click handler. - external dynamic attachClickHandler( - dynamic container, - SigninOptions options, - dynamic Function(GoogleUser googleUser) onsuccess, - dynamic Function(String reason) onfailure); -} - -@anonymous -@JS() -abstract class _GoogleAuth { - external Promise signIn( - [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); - external Promise grantOfflineAccess( - [OfflineAccessOptions? options]); -} - -extension GoogleAuthExtensions on GoogleAuth { - Future signIn( - [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]) { - final _GoogleAuth tt = this as _GoogleAuth; - return promiseToFuture(tt.signIn(options)); - } - - Future grantOfflineAccess( - [OfflineAccessOptions? options]) { - final _GoogleAuth tt = this as _GoogleAuth; - return promiseToFuture(tt.grantOfflineAccess(options)); - } -} - -@anonymous -@JS() -abstract class IsSignedIn { - /// Returns whether the current user is currently signed in. - external bool get(); - - /// Listen for changes in the current user's sign-in state. - external void listen(dynamic Function(bool signedIn) listener); -} - -@anonymous -@JS() -abstract class CurrentUser { - /// Returns a GoogleUser object that represents the current user. Note that in a newly-initialized - /// GoogleAuth instance, the current user has not been set. Use the currentUser.listen() method or the - /// GoogleAuth.then() to get an initialized GoogleAuth instance. - external GoogleUser get(); - - /// Listen for changes in currentUser. - external void listen(dynamic Function(GoogleUser user) listener); -} - -@anonymous -@JS() -abstract class SigninOptions { - external factory SigninOptions( - {String app_package_name, - bool fetch_basic_profile, - String prompt, - String scope, - String /*'popup'|'redirect'*/ ux_mode, - String redirect_uri, - String login_hint}); - - /// 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); - - /// 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); - - /// 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); - - /// 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); - - /// 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); - - /// 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); - - // 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); -} - -/// Definitions by: John -/// Interface that represents the different configuration parameters for the GoogleAuth.grantOfflineAccess(options) method. -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2offlineaccessoptions -@anonymous -@JS() -abstract class OfflineAccessOptions { - external factory OfflineAccessOptions( - {String scope, - String /*'select_account'|'consent'*/ prompt, - String app_package_name}); - 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); -} - -/// Interface that represents the different configuration parameters for the gapi.auth2.init method. -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2clientconfig -@anonymous -@JS() -abstract class ClientConfig { - external factory ClientConfig({ - String client_id, - String cookie_policy, - String scope, - bool fetch_basic_profile, - String? hosted_domain, - String openid_realm, - String /*'popup'|'redirect'*/ ux_mode, - String redirect_uri, - String plugin_name, - }); - - /// The app's client ID, found and created in the Google Developers Console. - 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); - - /// 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); - - /// 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); - - /// 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); - - /// 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); - - /// 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); - - /// 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); - - /// Allows newly created Client IDs to use the Google Platform Library from now until the March 30th, 2023 deprecation date. - /// See: https://github.com/flutter/flutter/issues/88084 - external String? get plugin_name; - external set plugin_name(String? v); -} - -@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(); -} - -/// 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); -} - -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeconfig -@anonymous -@JS() -abstract class AuthorizeConfig { - external factory AuthorizeConfig( - {String client_id, - String scope, - String response_type, - String prompt, - String cookie_policy, - String hosted_domain, - String login_hint, - String app_package_name, - String openid_realm, - bool include_granted_scopes}); - external String get client_id; - 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); -} - -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeresponse -@anonymous -@JS() -abstract class AuthorizeResponse { - external factory AuthorizeResponse( - {String access_token, - String id_token, - String code, - String scope, - num expires_in, - num first_issued_at, - num expires_at, - String error, - String error_subtype}); - 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 code; - external set code(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 error; - external set error(String v); - external String get error_subtype; - external set error_subtype(String v); -} - -/// A GoogleUser object represents one user account. -@anonymous -@JS() -abstract class GoogleUser { - /// Get the user's unique ID string. - 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(); - - /// Get the scopes that the user granted as a space-delimited string. - external String? getGrantedScopes(); - - /// Get the user's basic profile information. - 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); - - // 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]); - - /// Get permission from the user to access the specified scopes offline. - /// When you use GoogleUser.grantOfflineAccess(), the sign-in flow skips the account chooser step. - /// See GoogleUser.grantOfflineAccess(). - external void grantOfflineAccess(String scopes); - - /// Revokes all of the scopes that the user granted. - external void disconnect(); -} - -@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 _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') -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(); - -/// 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') -external void authorize( - AuthorizeConfig params, void Function(AuthorizeResponse response) callback); -// End module gapi.auth2 - -// Module gapi.signin2 -@JS('gapi.signin2.render') -external void render( - dynamic id, - dynamic - /*{ - /** - * The auth scope or scopes to authorize. Auth scopes for individual APIs can be found in their documentation. - */ - scope?: string; - - /** - * The width of the button in pixels (default: 120). - */ - width?: number; - - /** - * The height of the button in pixels (default: 36). - */ - height?: number; - - /** - * Display long labels such as "Sign in with Google" rather than "Sign in" (default: false). - */ - longtitle?: boolean; - - /** - * The color theme of the button: either light or dark (default: light). - */ - theme?: string; - - /** - * The callback function to call when a user successfully signs in (default: none). - */ - onsuccess?(user: auth2.GoogleUser): void; - - /** - * The callback function to call when sign-in fails (default: none). - */ - onfailure?(reason: { error: string }): void; - - /** - * The package name of the Android app to install over the air. See - * Android app installs from your web site. - * Optional. (default: none) - */ - app_package_name?: string; - }*/ - options); - -// End module gapi.signin2 -@JS() -abstract class Promise { - external factory Promise( - void Function(void Function(T result) resolve, Function reject) executor); -} 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 deleted file mode 100644 index 57b91838b8f1..000000000000 --- a/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@JS() -library gapi_onload; - -import 'dart:async'; - -import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:js/js.dart'; - -import 'js_interop/gapi.dart' as gapi; -import 'utils.dart' show injectJSLibraries; - -@JS() -external set gapiOnloadCallback(Function callback); - -// This name must match the external setter above -/// 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'; -String _addOnloadToScript(String url) => url.startsWith('data:') - ? url - : '$url?onload=$kGapiOnloadCallbackFunctionName'; - -/// Injects the GAPI library by its [url], and other additional [libraries]. -/// -/// GAPI has an onload API where it'll call a callback when it's ready, JSONP style. -Future inject(String url, {List libraries = const []}) { - // Inject the GAPI library, and configure the onload global - final Completer gapiOnLoad = Completer(); - gapiOnloadCallback = allowInterop(() { - // Funnel the GAPI onload to a Dart future - gapiOnLoad.complete(); - }); - - // Attach the onload callback to the main url - final List allLibraries = [ - _addOnloadToScript(url), - ...libraries - ]; - - return Future.wait( - >[injectJSLibraries(allLibraries), gapiOnLoad.future]); -} - -/// Initialize the global gapi object so 'auth2' can be used. -/// Returns a promise that resolves when 'auth2' is ready. -Future init() { - final Completer gapiLoadCompleter = Completer(); - gapi.load('auth2', allowInterop(() { - gapiLoadCompleter.complete(); - })); - - // After this resolves, we can use gapi.auth2! - return gapiLoadCompleter.future; -} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/people.dart b/packages/google_sign_in/google_sign_in_web/lib/src/people.dart new file mode 100644 index 000000000000..528dc89b1a75 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/people.dart @@ -0,0 +1,152 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:google_identity_services_web/oauth2.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:http/http.dart' as http; + +/// Basic scopes for self-id +const List scopes = [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email', +]; + +/// People API to return my profile info... +const String MY_PROFILE = 'https://content-people.googleapis.com/v1/people/me' + '?sources=READ_SOURCE_TYPE_PROFILE' + '&personFields=photos%2Cnames%2CemailAddresses'; + +/// Requests user data from the People API using the given [tokenResponse]. +Future requestUserData( + TokenResponse tokenResponse, { + @visibleForTesting http.Client? overrideClient, +}) async { + // Request my profile from the People API. + final Map person = await _doRequest( + MY_PROFILE, + tokenResponse, + overrideClient: overrideClient, + ); + + // Now transform the Person response into a GoogleSignInUserData. + return extractUserData(person); +} + +/// Extracts user data from a Person resource. +/// +/// See: https://developers.google.com/people/api/rest/v1/people#Person +GoogleSignInUserData? extractUserData(Map json) { + final String? userId = _extractUserId(json); + final String? email = _extractPrimaryField( + json['emailAddresses'] as List?, + 'value', + ); + + assert(userId != null); + assert(email != null); + + return GoogleSignInUserData( + id: userId!, + email: email!, + displayName: _extractPrimaryField( + json['names'] as List?, + 'displayName', + ), + photoUrl: _extractPrimaryField( + json['photos'] as List?, + 'url', + ), + // Synthetic user data doesn't contain an idToken! + ); +} + +/// Extracts the ID from a Person resource. +/// +/// The User ID looks like this: +/// { +/// 'resourceName': 'people/PERSON_ID', +/// ... +/// } +String? _extractUserId(Map profile) { + final String? resourceName = profile['resourceName'] as String?; + return resourceName?.split('/').last; +} + +/// Extracts the [fieldName] marked as 'primary' from a list of [values]. +/// +/// Values can be one of: +/// * `emailAddresses` +/// * `names` +/// * `photos` +/// +/// From a Person object. +T? _extractPrimaryField(List? values, String fieldName) { + if (values != null) { + for (final Object? value in values) { + if (value != null && value is Map) { + final bool isPrimary = _extractPath( + value, + path: ['metadata', 'primary'], + defaultValue: false, + ); + if (isPrimary) { + return value[fieldName] as T?; + } + } + } + } + + return null; +} + +/// Attempts to get the property in [path] of type `T` from a deeply nested [source]. +/// +/// Returns [default] if the property is not found. +T _extractPath( + Map source, { + required List path, + required T defaultValue, +}) { + final String valueKey = path.removeLast(); + Object? data = source; + for (final String key in path) { + if (data != null && data is Map) { + data = data[key]; + } else { + break; + } + } + if (data != null && data is Map) { + return (data[valueKey] ?? defaultValue) as T; + } else { + return defaultValue; + } +} + +/// Gets from [url] with an authorization header defined by [token]. +/// +/// Attempts to [jsonDecode] the result. +Future> _doRequest( + String url, + TokenResponse token, { + http.Client? overrideClient, +}) async { + final Uri uri = Uri.parse(url); + final http.Client client = overrideClient ?? http.Client(); + try { + final http.Response response = + await client.get(uri, headers: { + 'Authorization': '${token.token_type} ${token.access_token}', + }); + if (response.statusCode != 200) { + throw http.ClientException(response.body, uri); + } + return jsonDecode(response.body) as Map; + } finally { + client.close(); + } +} 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 45acb1ffd7ed..c4bb9d403d2d 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 @@ -2,59 +2,87 @@ // 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' as html; +import 'dart:convert'; +import 'package:google_identity_services_web/id.dart'; +import 'package:google_identity_services_web/oauth2.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'js_interop/gapiauth2.dart' as auth2; - -/// 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')!; - - for (final String library in libraries) { - final html.ScriptElement script = html.ScriptElement() - ..async = true - ..defer = true - // ignore: unsafe_html - ..src = library; - // TODO(ditman): add a timeout race to fail this future - loading.add(script.onLoad.first); - tags.add(script); +/// A codec that can encode/decode JWT payloads. +/// +/// See https://www.rfc-editor.org/rfc/rfc7519#section-3 +final Codec jwtCodec = json.fuse(utf8).fuse(base64); + +/// A RegExp that can match, and extract parts from a JWT Token. +/// +/// A JWT token consists of 3 base-64 encoded parts of data separated by periods: +/// +/// header.payload.signature +/// +/// More info: https://regexr.com/789qc +final RegExp jwtTokenRegexp = RegExp( + r'^(?
[^\.\s]+)\.(?[^\.\s]+)\.(?[^\.\s]+)$'); + +/// Decodes the `claims` of a JWT token and returns them as a Map. +/// +/// JWT `claims` are stored as a JSON object in the `payload` part of the token. +/// +/// (This method does not validate the signature of the token.) +/// +/// See https://www.rfc-editor.org/rfc/rfc7519#section-3 +Map? getJwtTokenPayload(String? token) { + if (token != null) { + final RegExpMatch? match = jwtTokenRegexp.firstMatch(token); + if (match != null) { + return decodeJwtPayload(match.namedGroup('payload')); + } } - targetElement.children.addAll(tags); - return Future.wait(loading); + return null; } -/// Utility method that converts `currentUser` to the equivalent [GoogleSignInUserData]. +/// Decodes a JWT payload using the [jwtCodec]. +Map? decodeJwtPayload(String? payload) { + try { + // Payload must be normalized before passing it to the codec + return jwtCodec.decode(base64.normalize(payload!)) as Map?; + } catch (_) { + // Do nothing, we always return null for any failure. + } + return null; +} + +/// Converts a [CredentialResponse] into a [GoogleSignInUserData]. /// -/// This method returns `null` when the [currentUser] is not signed in. -GoogleSignInUserData? gapiUserToPluginUserData(auth2.GoogleUser? currentUser) { - final bool isSignedIn = currentUser?.isSignedIn() ?? false; - final auth2.BasicProfile? profile = currentUser?.getBasicProfile(); - if (!isSignedIn || profile?.getId() == null) { +/// May return `null`, if the `credentialResponse` is null, or its `credential` +/// cannot be decoded. +GoogleSignInUserData? gisResponsesToUserData( + CredentialResponse? credentialResponse) { + if (credentialResponse == null || credentialResponse.credential == null) { + return null; + } + + final Map? payload = + getJwtTokenPayload(credentialResponse.credential); + + if (payload == null) { return null; } return GoogleSignInUserData( - displayName: profile?.getName(), - email: profile?.getEmail() ?? '', - id: profile?.getId() ?? '', - photoUrl: profile?.getImageUrl(), - idToken: currentUser?.getAuthResponse().id_token, + email: payload['email']! as String, + id: payload['sub']! as String, + displayName: payload['name']! as String, + photoUrl: payload['picture']! as String, + idToken: credentialResponse.credential, + ); +} + +/// Converts responses from the GIS library into TokenData for the plugin. +GoogleSignInTokenData gisResponsesToTokenData( + CredentialResponse? credentialResponse, TokenResponse? tokenResponse) { + return GoogleSignInTokenData( + idToken: credentialResponse?.credential, + accessToken: tokenResponse?.access_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 55bf760bcbf2..40e8b0381e67 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web. repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 0.10.2+1 +version: 0.11.0 environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + sdk: ">=2.17.0 <3.0.0" + flutter: ">=3.0.0" flutter: plugin: @@ -22,7 +22,9 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter + google_identity_services_web: ^0.2.0 google_sign_in_platform_interface: ^2.2.0 + http: ^0.13.5 js: ^0.6.3 dev_dependencies: diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 76192566b18b..1ac6a8d77ba2 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.8.6+2 + +* Updates `NSPhotoLibraryUsageDescription` description in README. + +* Updates minimum Flutter version to 3.0. + +## 0.8.6+1 + +* Updates code for stricter lint checks. + ## 0.8.6 * Updates minimum Flutter version to 2.10. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index aadfc83ff5e6..8fff8920054c 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -23,7 +23,7 @@ As a result of implementing PHPicker it becomes impossible to pick HEIC images o Add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: * `NSPhotoLibraryUsageDescription` - describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor. - * This permission is not required for image picking on iOS 11+ if you pass `false` for `requestFullMetadata`. + * This permission will not be requested if you always pass `false` for `requestFullMetadata`, but App Store policy requires including the plist entry. * `NSCameraUsageDescription` - describe why your app needs access to the camera. This is called _Privacy - Camera Usage Description_ in the visual editor. * `NSMicrophoneUsageDescription` - describe why your app needs access to the microphone, if you intend to record videos. This is called _Privacy - Microphone Usage Description_ in the visual editor. diff --git a/packages/image_picker/image_picker/example/android/gradle.properties b/packages/image_picker/image_picker/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100755 --- a/packages/image_picker/image_picker/example/android/gradle.properties +++ b/packages/image_picker/image_picker/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index 5e448ddbee68..f4f6546b1a98 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -260,7 +260,7 @@ class _MyHomePageState extends State { ); case ConnectionState.done: return _handlePreview(); - default: + case ConnectionState.active: if (snapshot.hasError) { return Text( 'Pick image/video error: ${snapshot.error}}', diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index e9511e27ab6d..3d97877498dc 100755 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 7fed3bf4637b..0d6308198891 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.6 +version: 0.8.6+2 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/image_picker/image_picker_android/CHANGELOG.md b/packages/image_picker/image_picker_android/CHANGELOG.md index b041761181d0..1ab21108d70f 100644 --- a/packages/image_picker/image_picker_android/CHANGELOG.md +++ b/packages/image_picker/image_picker_android/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.8.5+6 + +* Updates minimum Flutter version to 3.0. +* Fixes names of picked files to match original filenames where possible. + +## 0.8.5+5 + +* Updates code for stricter lint checks. + ## 0.8.5+4 * Fixes null cast exception when restoring a cancelled selection. diff --git a/packages/image_picker/image_picker_android/android/build.gradle b/packages/image_picker/image_picker_android/android/build.gradle index aed1ad5174ea..e61f3161d0f5 100644 --- a/packages/image_picker/image_picker_android/android/build.gradle +++ b/packages/image_picker/image_picker_android/android/build.gradle @@ -29,9 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } dependencies { implementation 'androidx.core:core:1.8.0' @@ -39,7 +37,7 @@ android { implementation 'androidx.exifinterface:exifinterface:1.3.3' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.8.0' + testImplementation 'org.mockito:mockito-core:5.1.1' testImplementation 'androidx.test:core:1.4.0' testImplementation "org.robolectric:robolectric:4.8.1" } diff --git a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java index 1f51a226c7e2..449480c19d9c 100644 --- a/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java +++ b/packages/image_picker/image_picker_android/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java @@ -25,55 +25,60 @@ import android.content.ContentResolver; import android.content.Context; +import android.database.Cursor; import android.net.Uri; +import android.provider.MediaStore; import android.webkit.MimeTypeMap; +import io.flutter.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.UUID; class FileUtils { - + /** + * Copies the file from the given content URI to a temporary directory, retaining the original + * file name if possible. + * + *

Each file is placed in its own directory to avoid conflicts according to the following + * scheme: {cacheDir}/{randomUuid}/{fileName} + * + *

If the original file name is unknown, a predefined "image_picker" filename is used and the + * file extension is deduced from the mime type (with fallback to ".jpg" in case of failure). + */ String getPathFromUri(final Context context, final Uri uri) { - File file = null; - InputStream inputStream = null; - OutputStream outputStream = null; - boolean success = false; - try { - String extension = getImageExtension(context, uri); - inputStream = context.getContentResolver().openInputStream(uri); - file = File.createTempFile("image_picker", extension, context.getCacheDir()); - file.deleteOnExit(); - outputStream = new FileOutputStream(file); - if (inputStream != null) { - copy(inputStream, outputStream); - success = true; + try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) { + String uuid = UUID.randomUUID().toString(); + File targetDirectory = new File(context.getCacheDir(), uuid); + targetDirectory.mkdir(); + // TODO(SynSzakala) according to the docs, `deleteOnExit` does not work reliably on Android; we should preferably + // just clear the picked files after the app startup. + targetDirectory.deleteOnExit(); + String fileName = getImageName(context, uri); + if (fileName == null) { + Log.w("FileUtils", "Cannot get file name for " + uri); + fileName = "image_picker" + getImageExtension(context, uri); } - } catch (IOException ignored) { - } finally { - try { - if (inputStream != null) inputStream.close(); - } catch (IOException ignored) { - } - try { - if (outputStream != null) outputStream.close(); - } catch (IOException ignored) { - // If closing the output stream fails, we cannot be sure that the - // target file was written in full. Flushing the stream merely moves - // the bytes into the OS, not necessarily to the file. - success = false; + File file = new File(targetDirectory, fileName); + try (OutputStream outputStream = new FileOutputStream(file)) { + copy(inputStream, outputStream); + return file.getPath(); } + } catch (IOException e) { + // If closing the output stream fails, we cannot be sure that the + // target file was written in full. Flushing the stream merely moves + // the bytes into the OS, not necessarily to the file. + return null; } - return success ? file.getPath() : null; } /** @return extension of image with dot, or default .jpg if it none. */ private static String getImageExtension(Context context, Uri uriImage) { - String extension = null; + String extension; try { - String imagePath = uriImage.getPath(); if (uriImage.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { final MimeTypeMap mime = MimeTypeMap.getSingleton(); extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriImage)); @@ -94,6 +99,20 @@ private static String getImageExtension(Context context, Uri uriImage) { return "." + extension; } + /** @return name of the image provided by ContentResolver; this may be null. */ + private static String getImageName(Context context, Uri uriImage) { + try (Cursor cursor = queryImageName(context, uriImage)) { + if (cursor == null || !cursor.moveToFirst() || cursor.getColumnCount() < 1) return null; + return cursor.getString(0); + } + } + + private static Cursor queryImageName(Context context, Uri uriImage) { + return context + .getContentResolver() + .query(uriImage, new String[] {MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null); + } + private static void copy(InputStream in, OutputStream out) throws IOException { final byte[] buffer = new byte[4 * 1024]; int bytesRead; diff --git a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java index 32e3ebc6183d..0ea0173fa954 100644 --- a/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java +++ b/packages/image_picker/image_picker_android/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java @@ -8,8 +8,15 @@ import static org.junit.Assert.assertTrue; import static org.robolectric.Shadows.shadowOf; +import android.content.ContentProvider; +import android.content.ContentValues; import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; import android.net.Uri; +import android.provider.MediaStore; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; @@ -19,6 +26,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; import org.robolectric.shadows.ShadowContentResolver; @@ -63,4 +71,62 @@ public void FileUtil_getImageExtension() throws IOException { String path = fileUtils.getPathFromUri(context, uri); assertTrue(path.endsWith(".jpg")); } + + @Test + public void FileUtil_getImageName() throws IOException { + Uri uri = Uri.parse("content://dummy/dummy.png"); + Robolectric.buildContentProvider(MockContentProvider.class).create("dummy"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("imageStream".getBytes(UTF_8))); + String path = fileUtils.getPathFromUri(context, uri); + assertTrue(path.endsWith("dummy.png")); + } + + private static class MockContentProvider extends ContentProvider { + + @Override + public boolean onCreate() { + return true; + } + + @Nullable + @Override + public Cursor query( + @NonNull Uri uri, + @Nullable String[] projection, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String sortOrder) { + MatrixCursor cursor = new MatrixCursor(new String[] {MediaStore.MediaColumns.DISPLAY_NAME}); + cursor.addRow(new Object[] {"dummy.png"}); + return cursor; + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + return "image/png"; + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + return null; + } + + @Override + public int delete( + @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { + return 0; + } + + @Override + public int update( + @NonNull Uri uri, + @Nullable ContentValues values, + @Nullable String selection, + @Nullable String[] selectionArgs) { + return 0; + } + } } diff --git a/packages/image_picker/image_picker_android/example/android/app/build.gradle b/packages/image_picker/image_picker_android/example/android/app/build.gradle index 31d8c82a0a9d..f8487c7959f1 100755 --- a/packages/image_picker/image_picker_android/example/android/app/build.gradle +++ b/packages/image_picker/image_picker_android/example/android/app/build.gradle @@ -63,5 +63,7 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation project(':image_picker_android') + implementation project(':espresso') api 'androidx.test:core:1.4.0' } diff --git a/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerPickTest.java b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerPickTest.java new file mode 100644 index 000000000000..8b7ae11d5c2d --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerPickTest.java @@ -0,0 +1,43 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import static androidx.test.espresso.flutter.EspressoFlutter.onFlutterWidget; +import static androidx.test.espresso.flutter.action.FlutterActions.click; +import static androidx.test.espresso.flutter.assertion.FlutterAssertions.matches; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withText; +import static androidx.test.espresso.flutter.matcher.FlutterMatchers.withValueKey; +import static androidx.test.espresso.intent.Intents.intended; +import static androidx.test.espresso.intent.Intents.intending; +import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; + +import android.app.Activity; +import android.app.Instrumentation; +import android.content.Intent; +import android.net.Uri; +import androidx.test.espresso.intent.rule.IntentsTestRule; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +public class ImagePickerPickTest { + + @Rule public TestRule rule = new IntentsTestRule<>(DriverExtensionActivity.class); + + @Test + @Ignore("Doesn't run in Firebase Test Lab: https://github.com/flutter/flutter/issues/94748") + public void imageIsPickedWithOriginalName() { + Instrumentation.ActivityResult result = + new Instrumentation.ActivityResult( + Activity.RESULT_OK, new Intent().setData(Uri.parse("content://dummy/dummy.png"))); + intending(hasAction(Intent.ACTION_GET_CONTENT)).respondWith(result); + onFlutterWidget(withValueKey("image_picker_example_from_gallery")).perform(click()); + onFlutterWidget(withText("PICK")).perform(click()); + intended(hasAction(Intent.ACTION_GET_CONTENT)); + onFlutterWidget(withValueKey("image_picker_example_picked_image_name")) + .check(matches(withText("dummy.png"))); + } +} diff --git a/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml b/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml index 6f85cefded34..317af1d1a371 100644 --- a/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml +++ b/packages/image_picker/image_picker_android/example/android/app/src/debug/AndroidManifest.xml @@ -13,5 +13,17 @@ android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize"> + + + diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DriverExtensionActivity.java b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DriverExtensionActivity.java new file mode 100644 index 000000000000..b35a6c4b0e49 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DriverExtensionActivity.java @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; + +public class DriverExtensionActivity extends FlutterActivity { + @NonNull + @Override + public String getDartEntrypointFunctionName() { + return "appMain"; + } +} diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DummyContentProvider.java b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DummyContentProvider.java new file mode 100644 index 000000000000..8967318ee977 --- /dev/null +++ b/packages/image_picker/image_picker_android/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/DummyContentProvider.java @@ -0,0 +1,68 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.provider.MediaStore; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class DummyContentProvider extends ContentProvider { + @Override + public boolean onCreate() { + return true; + } + + @Nullable + @Override + public AssetFileDescriptor openAssetFile(@NonNull Uri uri, @NonNull String mode) { + return getContext().getResources().openRawResourceFd(R.raw.ic_launcher); + } + + @Nullable + @Override + public Cursor query( + @NonNull Uri uri, + @Nullable String[] projection, + @Nullable String selection, + @Nullable String[] selectionArgs, + @Nullable String sortOrder) { + MatrixCursor cursor = new MatrixCursor(new String[] {MediaStore.MediaColumns.DISPLAY_NAME}); + cursor.addRow(new Object[] {"dummy.png"}); + return cursor; + } + + @Nullable + @Override + public String getType(@NonNull Uri uri) { + return "image/png"; + } + + @Nullable + @Override + public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { + return null; + } + + @Override + public int delete( + @NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) { + return 0; + } + + @Override + public int update( + @NonNull Uri uri, + @Nullable ContentValues values, + @Nullable String selection, + @Nullable String[] selectionArgs) { + return 0; + } +} diff --git a/packages/image_picker/image_picker_android/example/android/app/src/main/res/raw/ic_launcher.png b/packages/image_picker/image_picker_android/example/android/app/src/main/res/raw/ic_launcher.png new file mode 100755 index 000000000000..17987b79bb8a Binary files /dev/null and b/packages/image_picker/image_picker_android/example/android/app/src/main/res/raw/ic_launcher.png differ diff --git a/packages/image_picker/image_picker_android/example/android/gradle.properties b/packages/image_picker/image_picker_android/example/android/gradle.properties index 94adc3a3f97a..598d13fee446 100755 --- a/packages/image_picker/image_picker_android/example/android/gradle.properties +++ b/packages/image_picker/image_picker_android/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/image_picker/image_picker_android/example/lib/main.dart b/packages/image_picker/image_picker_android/example/lib/main.dart index 212e064cc6e5..34f9114332f5 100755 --- a/packages/image_picker/image_picker_android/example/lib/main.dart +++ b/packages/image_picker/image_picker_android/example/lib/main.dart @@ -9,9 +9,15 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_driver/driver_extension.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; import 'package:video_player/video_player.dart'; +void appMain() { + enableFlutterDriverExtension(); + main(); +} + void main() { runApp(const MyApp()); } @@ -80,17 +86,23 @@ class _MyHomePageState extends State { } } - Future _onImageButtonPressed(ImageSource source, - {BuildContext? context, bool isMultiImage = false}) async { + Future _onImageButtonPressed( + BuildContext context, { + required ImageSource source, + bool isMultiImage = false, + }) async { if (_controller != null) { await _controller!.setVolume(0.0); } if (isVideo) { final XFile? file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); + if (file != null && context.mounted) { + _showPickedSnackBar(context, [file]); + } await _playVideo(file); - } else if (isMultiImage) { - await _displayPickImageDialog(context!, + } else if (isMultiImage && context.mounted) { + await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { final List? pickedFileList = await _picker.getMultiImage( @@ -98,17 +110,16 @@ class _MyHomePageState extends State { maxHeight: maxHeight, imageQuality: quality, ); - setState(() { - _imageFileList = pickedFileList; - }); + if (pickedFileList != null && context.mounted) { + _showPickedSnackBar(context, pickedFileList); + } + setState(() => _imageFileList = pickedFileList); } catch (e) { - setState(() { - _pickImageError = e; - }); + setState(() => _pickImageError = e); } }); } else { - await _displayPickImageDialog(context!, + await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { final XFile? pickedFile = await _picker.getImage( @@ -117,13 +128,12 @@ class _MyHomePageState extends State { maxHeight: maxHeight, imageQuality: quality, ); - setState(() { - _setImageFileListFromFile(pickedFile); - }); + if (pickedFile != null && context.mounted) { + _showPickedSnackBar(context, [pickedFile]); + } + setState(() => _setImageFileListFromFile(pickedFile)); } catch (e) { - setState(() { - _pickImageError = e; - }); + setState(() => _pickImageError = e); } }); } @@ -183,13 +193,21 @@ class _MyHomePageState extends State { child: ListView.builder( key: UniqueKey(), itemBuilder: (BuildContext context, int index) { - // Why network for web? - // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform - return Semantics( - label: 'image_picker_example_picked_image', - child: kIsWeb - ? Image.network(_imageFileList![index].path) - : Image.file(File(_imageFileList![index].path)), + final XFile image = _imageFileList![index]; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(image.name, + key: const Key('image_picker_example_picked_image_name')), + // Why network for web? + // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform + Semantics( + label: 'image_picker_example_picked_image', + child: kIsWeb + ? Image.network(image.path) + : Image.file(File(image.path)), + ), + ], ); }, itemCount: _imageFileList!.length, @@ -260,7 +278,7 @@ class _MyHomePageState extends State { ); case ConnectionState.done: return _handlePreview(); - default: + case ConnectionState.active: if (snapshot.hasError) { return Text( 'Pick image/video error: ${snapshot.error}}', @@ -283,9 +301,10 @@ class _MyHomePageState extends State { Semantics( label: 'image_picker_example_from_gallery', child: FloatingActionButton( + key: const Key('image_picker_example_from_gallery'), onPressed: () { isVideo = false; - _onImageButtonPressed(ImageSource.gallery, context: context); + _onImageButtonPressed(context, source: ImageSource.gallery); }, heroTag: 'image0', tooltip: 'Pick Image from gallery', @@ -298,8 +317,8 @@ class _MyHomePageState extends State { onPressed: () { isVideo = false; _onImageButtonPressed( - ImageSource.gallery, - context: context, + context, + source: ImageSource.gallery, isMultiImage: true, ); }, @@ -313,7 +332,7 @@ class _MyHomePageState extends State { child: FloatingActionButton( onPressed: () { isVideo = false; - _onImageButtonPressed(ImageSource.camera, context: context); + _onImageButtonPressed(context, source: ImageSource.camera); }, heroTag: 'image2', tooltip: 'Take a Photo', @@ -326,7 +345,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { isVideo = true; - _onImageButtonPressed(ImageSource.gallery); + _onImageButtonPressed(context, source: ImageSource.gallery); }, heroTag: 'video0', tooltip: 'Pick Video from gallery', @@ -339,7 +358,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { isVideo = true; - _onImageButtonPressed(ImageSource.camera); + _onImageButtonPressed(context, source: ImageSource.camera); }, heroTag: 'video1', tooltip: 'Take a Video', @@ -417,6 +436,13 @@ class _MyHomePageState extends State { ); }); } + + void _showPickedSnackBar(BuildContext context, List files) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Picked: ${files.map((XFile it) => it.name).join(',')}'), + duration: const Duration(seconds: 2), + )); + } } typedef OnPickImageCallback = void Function( diff --git a/packages/image_picker/image_picker_android/example/pubspec.yaml b/packages/image_picker/image_picker_android/example/pubspec.yaml index 02ef8a02af4c..bfeac3de14d5 100755 --- a/packages/image_picker/image_picker_android/example/pubspec.yaml +++ b/packages/image_picker/image_picker_android/example/pubspec.yaml @@ -4,11 +4,13 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: sdk: flutter + flutter_driver: + sdk: flutter flutter_plugin_android_lifecycle: ^2.0.1 image_picker_android: # When depending on this package from a real application you should use: @@ -22,8 +24,6 @@ dependencies: dev_dependencies: espresso: ^0.2.0 - flutter_driver: - sdk: flutter flutter_test: sdk: flutter integration_test: diff --git a/packages/image_picker/image_picker_android/pubspec.yaml b/packages/image_picker/image_picker_android/pubspec.yaml index 4c32e345007c..a0516685964c 100755 --- a/packages/image_picker/image_picker_android/pubspec.yaml +++ b/packages/image_picker/image_picker_android/pubspec.yaml @@ -2,11 +2,11 @@ name: image_picker_android description: Android implementation of the image_picker plugin. repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.5+4 +version: 0.8.5+6 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart index ee1eb79f1045..d6680ce44dd5 100644 --- a/packages/image_picker/image_picker_android/test/image_picker_android_test.dart +++ b/packages/image_picker/image_picker_android/test/image_picker_android_test.dart @@ -18,7 +18,10 @@ void main() { setUp(() { returnValue = ''; - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { log.add(methodCall); return returnValue; }); @@ -189,7 +192,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.pickImage(source: ImageSource.gallery), isNull); expect(await picker.pickImage(source: ImageSource.camera), isNull); @@ -347,7 +353,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.pickMultiImage(), isNull); expect(await picker.pickMultiImage(), isNull); @@ -418,7 +427,10 @@ void main() { }); test('handles a null video path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.pickVideo(source: ImageSource.gallery), isNull); expect(await picker.pickVideo(source: ImageSource.camera), isNull); @@ -460,7 +472,10 @@ void main() { group('#retrieveLostData', () { test('retrieveLostData get success response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'image', 'path': '/example/path', @@ -473,7 +488,10 @@ void main() { }); test('retrieveLostData get error response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -488,14 +506,20 @@ void main() { }); test('retrieveLostData get null response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return null; }); expect((await picker.retrieveLostData()).isEmpty, true); }); test('retrieveLostData get both path and error should throw', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -665,7 +689,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getImage(source: ImageSource.gallery), isNull); expect(await picker.getImage(source: ImageSource.camera), isNull); @@ -823,7 +850,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getMultiImage(), isNull); expect(await picker.getMultiImage(), isNull); @@ -894,7 +924,10 @@ void main() { }); test('handles a null video path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getVideo(source: ImageSource.gallery), isNull); expect(await picker.getVideo(source: ImageSource.camera), isNull); @@ -936,7 +969,10 @@ void main() { group('#getLostData', () { test('getLostData get success response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'image', 'path': '/example/path', @@ -949,7 +985,10 @@ void main() { }); test('getLostData should successfully retrieve multiple files', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'image', 'path': '/example/path1', @@ -965,7 +1004,10 @@ void main() { }); test('getLostData get error response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -980,14 +1022,20 @@ void main() { }); test('getLostData get null response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return null; }); expect((await picker.getLostData()).isEmpty, true); }); test('getLostData get both path and error should throw', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -1183,7 +1231,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect( await picker.getImageFromSource(source: ImageSource.gallery), isNull); @@ -1254,3 +1305,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index 8a5c089ef807..86c1bea873ae 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.1.10 * Updates code for `no_leading_underscores_for_local_identifiers` lint. diff --git a/packages/image_picker/image_picker_for_web/example/pubspec.yaml b/packages/image_picker/image_picker_for_web/example/pubspec.yaml index c39bd81f9de0..96ce0dfa70c7 100644 --- a/packages/image_picker/image_picker_for_web/example/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index c2e0975dda57..03c0fb3e3056 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.1.10 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/image_picker/image_picker_ios/CHANGELOG.md b/packages/image_picker/image_picker_ios/CHANGELOG.md index 9720dba0a305..dbd5160edd7d 100644 --- a/packages/image_picker/image_picker_ios/CHANGELOG.md +++ b/packages/image_picker/image_picker_ios/CHANGELOG.md @@ -1,3 +1,16 @@ +## 0.8.6+8 + +* Fixes issue with images sometimes changing to incorrect orientation. + +## 0.8.6+7 + +* Fixes issue where GIF file would not animate without `Photo Library Usage` permissions. Fixes issue where PNG and GIF files were converted to JPG, but only when they are do not have `Photo Library Usage` permissions. +* Updates minimum Flutter version to 3.0. + +## 0.8.6+6 + +* Updates code for stricter lint checks. + ## 0.8.6+5 * Fixes crash when `imageQuality` is set. diff --git a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.pbxproj index 2c97bd1d667f..ddbc856d6aa7 100644 --- a/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/image_picker/image_picker_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 680049382280F2B9006DD6AB /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; }; 680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; }; 6801C8392555D726009DAF8D /* ImagePickerFromGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */; }; + 782C2B45299ECE33008DC703 /* jpgImageWithRightOrientation.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 782C2B44299ECE33008DC703 /* jpgImageWithRightOrientation.jpg */; }; + 782C2B46299ECE33008DC703 /* jpgImageWithRightOrientation.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 782C2B44299ECE33008DC703 /* jpgImageWithRightOrientation.jpg */; }; 7865C5E12941326F0010E17F /* bmpImage.bmp in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5E02941326F0010E17F /* bmpImage.bmp */; }; 7865C5E22941326F0010E17F /* bmpImage.bmp in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5E02941326F0010E17F /* bmpImage.bmp */; }; 7865C5E4294132D50010E17F /* svgImage.svg in Resources */ = {isa = PBXBuildFile; fileRef = 7865C5E3294132D50010E17F /* svgImage.svg */; }; @@ -95,6 +97,7 @@ 6801C83A2555D726009DAF8D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImagePickerPluginTests.m; sourceTree = ""; }; 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PhotoAssetUtilTests.m; sourceTree = ""; }; + 782C2B44299ECE33008DC703 /* jpgImageWithRightOrientation.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = jpgImageWithRightOrientation.jpg; sourceTree = ""; }; 7865C5E02941326F0010E17F /* bmpImage.bmp */ = {isa = PBXFileReference; lastKnownFileType = image.bmp; path = bmpImage.bmp; sourceTree = ""; }; 7865C5E3294132D50010E17F /* svgImage.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = svgImage.svg; sourceTree = ""; }; 7865C5E62941374F0010E17F /* heicImage.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = heicImage.heic; sourceTree = ""; }; @@ -169,6 +172,7 @@ 680049282280E33D006DD6AB /* TestImages */ = { isa = PBXGroup; children = ( + 782C2B44299ECE33008DC703 /* jpgImageWithRightOrientation.jpg */, 86E9A88F272747B90017E6E0 /* webpImage.webp */, 9FC8F0E8229FA49E00C8D58F /* gifImage.gif */, 680049362280F2B8006DD6AB /* jpgImage.jpg */, @@ -398,6 +402,7 @@ 86E9A894272754A30017E6E0 /* webpImage.webp in Resources */, 86E9A895272769130017E6E0 /* pngImage.png in Resources */, 7865C5FC294157BC0010E17F /* icnsImage.icns in Resources */, + 782C2B45299ECE33008DC703 /* jpgImageWithRightOrientation.jpg in Resources */, 86E9A896272769150017E6E0 /* jpgImage.jpg in Resources */, 7865C5ED294137AB0010E17F /* tiffImage.tiff in Resources */, ); @@ -409,6 +414,7 @@ files = ( 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */, 7865C5EE294137AB0010E17F /* tiffImage.tiff in Resources */, + 782C2B46299ECE33008DC703 /* jpgImageWithRightOrientation.jpg in Resources */, 7865C5E82941374F0010E17F /* heicImage.heic in Resources */, 7865C5FD294157BC0010E17F /* icnsImage.icns in Resources */, 680049382280F2B9006DD6AB /* pngImage.png in Resources */, diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m index e449a84b80bb..1dc807a15dba 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/ImageUtilTests.m @@ -36,12 +36,21 @@ - (void)testScaledImage_ShouldBeScaledWithNoMetadata { } - (void)testScaledImage_ShouldBeCorrectRotation { - UIImage *image = [UIImage imageWithData:ImagePickerTestImages.JPGTestData]; + NSURL *imageURL = + [[NSBundle bundleForClass:[self class]] URLForResource:@"jpgImageWithRightOrientation" + withExtension:@"jpg"]; + NSData *imageData = [NSData dataWithContentsOfURL:imageURL]; + UIImage *image = [UIImage imageWithData:imageData]; + XCTAssertEqual(image.size.width, 130); + XCTAssertEqual(image.size.height, 174); + XCTAssertEqual(image.imageOrientation, UIImageOrientationRight); + UIImage *newImage = [FLTImagePickerImageUtil scaledImage:image - maxWidth:@3 - maxHeight:@2 + maxWidth:@10 + maxHeight:@10 isMetadataAvailable:YES]; - + XCTAssertEqual(newImage.size.width, 10); + XCTAssertEqual(newImage.size.height, 7); XCTAssertEqual(newImage.imageOrientation, UIImageOrientationUp); } diff --git a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m index 091755ca163b..57ccb86c0060 100644 --- a/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m +++ b/packages/image_picker/image_picker_ios/example/ios/RunnerTests/PickerSaveImageToPathOperationTests.m @@ -21,7 +21,7 @@ - (void)testSaveWebPImage API_AVAILABLE(ios(14)) { NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; } - (void)testSavePNGImage API_AVAILABLE(ios(14)) { @@ -30,7 +30,7 @@ - (void)testSavePNGImage API_AVAILABLE(ios(14)) { NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"png"]; } - (void)testSaveJPGImage API_AVAILABLE(ios(14)) { @@ -39,7 +39,7 @@ - (void)testSaveJPGImage API_AVAILABLE(ios(14)) { NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; } - (void)testSaveGIFImage API_AVAILABLE(ios(14)) { @@ -48,7 +48,39 @@ - (void)testSaveGIFImage API_AVAILABLE(ios(14)) { NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + NSData *dataGIF = [NSData dataWithContentsOfURL:imageURL]; + CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)dataGIF, nil); + size_t numberOfFrames = CGImageSourceGetCount(imageSource); + + XCTestExpectation *pathExpectation = [self expectationWithDescription:@"Path was created"]; + XCTestExpectation *operationExpectation = + [self expectationWithDescription:@"Operation completed"]; + + FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc] + initWithResult:result + maxHeight:@100 + maxWidth:@100 + desiredImageQuality:@100 + fullMetadata:NO + savedPathBlock:^(NSString *savedPath, FlutterError *error) { + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:savedPath]); + + // Ensure gif is animated. + XCTAssertEqualObjects([NSURL URLWithString:savedPath].pathExtension, @"gif"); + NSData *newDataGIF = [NSData dataWithContentsOfFile:savedPath]; + CGImageSourceRef newImageSource = + CGImageSourceCreateWithData((__bridge CFDataRef)newDataGIF, nil); + size_t newNumberOfFrames = CGImageSourceGetCount(newImageSource); + XCTAssertEqual(numberOfFrames, newNumberOfFrames); + [pathExpectation fulfill]; + }]; + operation.completionBlock = ^{ + [operationExpectation fulfill]; + }; + + [operation start]; + [self waitForExpectationsWithTimeout:30 handler:nil]; + XCTAssertTrue(operation.isFinished); } - (void)testSaveBMPImage API_AVAILABLE(ios(14)) { @@ -57,7 +89,7 @@ - (void)testSaveBMPImage API_AVAILABLE(ios(14)) { NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; } - (void)testSaveHEICImage API_AVAILABLE(ios(14)) { @@ -66,7 +98,44 @@ - (void)testSaveHEICImage API_AVAILABLE(ios(14)) { NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; +} + +- (void)testSaveWithOrientation API_AVAILABLE(ios(14)) { + NSURL *imageURL = + [[NSBundle bundleForClass:[self class]] URLForResource:@"jpgImageWithRightOrientation" + withExtension:@"jpg"]; + NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; + PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; + + XCTestExpectation *pathExpectation = [self expectationWithDescription:@"Path was created"]; + XCTestExpectation *operationExpectation = + [self expectationWithDescription:@"Operation completed"]; + + FLTPHPickerSaveImageToPathOperation *operation = [[FLTPHPickerSaveImageToPathOperation alloc] + initWithResult:result + maxHeight:@10 + maxWidth:@10 + desiredImageQuality:@100 + fullMetadata:NO + savedPathBlock:^(NSString *savedPath, FlutterError *error) { + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:savedPath]); + + // Ensure image retained it's orientation data. + XCTAssertEqualObjects([NSURL URLWithString:savedPath].pathExtension, @"jpg"); + UIImage *image = [UIImage imageWithContentsOfFile:savedPath]; + XCTAssertEqual(image.imageOrientation, UIImageOrientationRight); + XCTAssertEqual(image.size.width, 7); + XCTAssertEqual(image.size.height, 10); + [pathExpectation fulfill]; + }]; + operation.completionBlock = ^{ + [operationExpectation fulfill]; + }; + + [operation start]; + [self waitForExpectationsWithTimeout:30 handler:nil]; + XCTAssertTrue(operation.isFinished); } - (void)testSaveICNSImage API_AVAILABLE(ios(14)) { @@ -75,7 +144,7 @@ - (void)testSaveICNSImage API_AVAILABLE(ios(14)) { NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; } - (void)testSaveICOImage API_AVAILABLE(ios(14)) { @@ -84,7 +153,7 @@ - (void)testSaveICOImage API_AVAILABLE(ios(14)) { NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; } - (void)testSaveProRAWImage API_AVAILABLE(ios(14)) { @@ -93,7 +162,7 @@ - (void)testSaveProRAWImage API_AVAILABLE(ios(14)) { NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; } - (void)testSaveSVGImage API_AVAILABLE(ios(14)) { @@ -102,7 +171,7 @@ - (void)testSaveSVGImage API_AVAILABLE(ios(14)) { NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; } - (void)testSaveTIFFImage API_AVAILABLE(ios(14)) { @@ -110,7 +179,7 @@ - (void)testSaveTIFFImage API_AVAILABLE(ios(14)) { withExtension:@"tiff"]; NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithContentsOfURL:imageURL]; PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; - [self verifySavingImageWithPickerResult:result fullMetadata:YES]; + [self verifySavingImageWithPickerResult:result fullMetadata:YES withExtension:@"jpg"]; } - (void)testNonexistentImage API_AVAILABLE(ios(14)) { @@ -176,7 +245,7 @@ - (void)testSavePNGImageWithoutFullMetadata API_AVAILABLE(ios(14)) { PHPickerResult *result = [self createPickerResultWithProvider:itemProvider]; OCMReject([photoAssetUtil fetchAssetsWithLocalIdentifiers:OCMOCK_ANY options:OCMOCK_ANY]); - [self verifySavingImageWithPickerResult:result fullMetadata:NO]; + [self verifySavingImageWithPickerResult:result fullMetadata:NO withExtension:@"png"]; OCMVerifyAll(photoAssetUtil); } @@ -204,7 +273,8 @@ - (PHPickerResult *)createPickerResultWithProvider:(NSItemProvider *)itemProvide * @param result the picker result */ - (void)verifySavingImageWithPickerResult:(PHPickerResult *)result - fullMetadata:(BOOL)fullMetadata API_AVAILABLE(ios(14)) { + fullMetadata:(BOOL)fullMetadata + withExtension:(NSString *)extension API_AVAILABLE(ios(14)) { XCTestExpectation *pathExpectation = [self expectationWithDescription:@"Path was created"]; XCTestExpectation *operationExpectation = [self expectationWithDescription:@"Operation completed"]; @@ -217,6 +287,7 @@ - (void)verifySavingImageWithPickerResult:(PHPickerResult *)result fullMetadata:fullMetadata savedPathBlock:^(NSString *savedPath, FlutterError *error) { XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:savedPath]); + XCTAssertEqualObjects([NSURL URLWithString:savedPath].pathExtension, extension); [pathExpectation fulfill]; }]; operation.completionBlock = ^{ diff --git a/packages/image_picker/image_picker_ios/example/ios/TestImages/jpgImageWithRightOrientation.jpg b/packages/image_picker/image_picker_ios/example/ios/TestImages/jpgImageWithRightOrientation.jpg new file mode 100644 index 000000000000..2b3eaf5e2944 Binary files /dev/null and b/packages/image_picker/image_picker_ios/example/ios/TestImages/jpgImageWithRightOrientation.jpg differ diff --git a/packages/image_picker/image_picker_ios/example/pubspec.yaml b/packages/image_picker/image_picker_ios/example/pubspec.yaml index 856f775cc641..bebe9bb04648 100755 --- a/packages/image_picker/image_picker_ios/example/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m index 11fedfb73846..80e03ddd6578 100644 --- a/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m +++ b/packages/image_picker/image_picker_ios/ios/Classes/FLTPHPickerSaveImageToPathOperation.m @@ -98,8 +98,7 @@ - (void)start { completionHandler:^(NSData *_Nullable data, NSError *_Nullable error) { if (data != nil) { - UIImage *image = [[UIImage alloc] initWithData:data]; - [self processImage:image]; + [self processImage:data]; } else { FlutterError *flutterError = [FlutterError errorWithCode:@"invalid_image" @@ -122,7 +121,9 @@ - (void)start { /** * Processes the image. */ -- (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) { +- (void)processImage:(NSData *)pickerImageData API_AVAILABLE(ios(14)) { + UIImage *localImage = [[UIImage alloc] initWithData:pickerImageData]; + PHAsset *originalAsset; // Only if requested, fetch the full "PHAsset" metadata, which requires "Photo Library Usage" // permissions. @@ -134,7 +135,7 @@ - (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) { localImage = [FLTImagePickerImageUtil scaledImage:localImage maxWidth:self.maxWidth maxHeight:self.maxHeight - isMetadataAvailable:originalAsset != nil]; + isMetadataAvailable:YES]; } if (originalAsset) { void (^resultHandler)(NSData *imageData, NSString *dataUTI, NSDictionary *info) = @@ -172,10 +173,13 @@ - (void)processImage:(UIImage *)localImage API_AVAILABLE(ios(14)) { } } else { // Image picked without an original asset (e.g. User pick image without permission) + // maxWidth and maxHeight are used only for GIF images. NSString *savedPath = - [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil - image:localImage - imageQuality:self.desiredImageQuality]; + [FLTImagePickerPhotoAssetUtil saveImageWithOriginalImageData:pickerImageData + image:localImage + maxWidth:self.maxWidth + maxHeight:self.maxHeight + imageQuality:self.desiredImageQuality]; [self completeOperationWithPath:savedPath error:nil]; } } diff --git a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart index fbc356f212b8..3f76784ff07c 100644 --- a/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart +++ b/packages/image_picker/image_picker_ios/lib/image_picker_ios.dart @@ -14,9 +14,14 @@ SourceType _convertSource(ImageSource source) { return SourceType.camera; case ImageSource.gallery: return SourceType.gallery; - default: - throw UnimplementedError('Unknown source: $source'); } + // The enum comes from a different package, which could get a new value at + // any time, so a fallback case is necessary. Since there is no reasonable + // default behavior, throw to alert the client that they need an updated + // version. This is deliberately outside the switch rather than a `default` + // so that the linter will flag the switch as needing an update. + // ignore: dead_code + throw UnimplementedError('Unknown source: $source'); } // Converts a [CameraDevice] to the corresponding Pigeon API enum value. @@ -26,9 +31,14 @@ SourceCamera _convertCamera(CameraDevice camera) { return SourceCamera.front; case CameraDevice.rear: return SourceCamera.rear; - default: - throw UnimplementedError('Unknown camera: $camera'); } + // The enum comes from a different package, which could get a new value at + // any time, so a fallback case is necessary. Since there is no reasonable + // default behavior, throw to alert the client that they need an updated + // version. This is deliberately outside the switch rather than a `default` + // so that the linter will flag the switch as needing an update. + // ignore: dead_code + throw UnimplementedError('Unknown camera: $camera'); } /// An implementation of [ImagePickerPlatform] for iOS. diff --git a/packages/image_picker/image_picker_ios/pigeons/messages.dart b/packages/image_picker/image_picker_ios/pigeons/messages.dart index dd8a0f0c0834..d04841b0fde9 100644 --- a/packages/image_picker/image_picker_ios/pigeons/messages.dart +++ b/packages/image_picker/image_picker_ios/pigeons/messages.dart @@ -6,7 +6,7 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', - dartTestOut: 'test/test_api.dart', + dartTestOut: 'test/test_api.g.dart', objcHeaderOut: 'ios/Classes/messages.g.h', objcSourceOut: 'ios/Classes/messages.g.m', objcOptions: ObjcOptions( diff --git a/packages/image_picker/image_picker_ios/pubspec.yaml b/packages/image_picker/image_picker_ios/pubspec.yaml index 91d96c3d0a20..b188055087c7 100755 --- a/packages/image_picker/image_picker_ios/pubspec.yaml +++ b/packages/image_picker/image_picker_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: image_picker_ios description: iOS implementation of the image_picker plugin. repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.8.6+5 +version: 0.8.6+8 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart index b20025770ad1..2c9d52509f26 100644 --- a/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart +++ b/packages/image_picker/image_picker_ios/test/image_picker_ios_test.dart @@ -8,7 +8,7 @@ import 'package:image_picker_ios/image_picker_ios.dart'; import 'package:image_picker_ios/src/messages.g.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -import 'test_api.dart'; +import 'test_api.g.dart'; @immutable class _LoggedMethodCall { diff --git a/packages/image_picker/image_picker_ios/test/test_api.dart b/packages/image_picker/image_picker_ios/test/test_api.g.dart similarity index 100% rename from packages/image_picker/image_picker_ios/test/test_api.dart rename to packages/image_picker/image_picker_ios/test/test_api.g.dart diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index 05b03a37cb98..91d6d80e6c23 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.6.2 * Updates imports for `prefer_relative_imports`. diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index eb4d2b649eac..2f34ee2b349c 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 2.6.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: cross_file: ^0.3.1+1 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 44980100742a..244af3982672 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 @@ -18,7 +18,10 @@ void main() { setUp(() { returnValue = ''; - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { log.add(methodCall); return returnValue; }); @@ -185,8 +188,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.pickImage(source: ImageSource.gallery), isNull); expect(await picker.pickImage(source: ImageSource.camera), isNull); @@ -352,8 +357,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.pickMultiImage(), isNull); expect(await picker.pickMultiImage(), isNull); @@ -424,8 +431,10 @@ void main() { }); test('handles a null video path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.pickVideo(source: ImageSource.gallery), isNull); expect(await picker.pickVideo(source: ImageSource.camera), isNull); @@ -467,7 +476,10 @@ void main() { group('#retrieveLostData', () { test('retrieveLostData get success response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'image', 'path': '/example/path', @@ -480,7 +492,10 @@ void main() { }); test('retrieveLostData get error response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -495,14 +510,20 @@ void main() { }); test('retrieveLostData get null response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return null; }); expect((await picker.retrieveLostData()).isEmpty, true); }); test('retrieveLostData get both path and error should throw', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -672,8 +693,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getImage(source: ImageSource.gallery), isNull); expect(await picker.getImage(source: ImageSource.camera), isNull); @@ -839,8 +862,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getMultiImage(), isNull); expect(await picker.getMultiImage(), isNull); @@ -911,8 +936,10 @@ void main() { }); test('handles a null video path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getVideo(source: ImageSource.gallery), isNull); expect(await picker.getVideo(source: ImageSource.camera), isNull); @@ -954,7 +981,10 @@ void main() { group('#getLostData', () { test('getLostData get success response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'image', 'path': '/example/path', @@ -967,7 +997,10 @@ void main() { }); test('getLostData should successfully retrieve multiple files', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'image', 'path': '/example/path1', @@ -983,7 +1016,10 @@ void main() { }); test('getLostData get error response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -998,14 +1034,20 @@ void main() { }); test('getLostData get null response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return null; }); expect((await picker.getLostData()).isEmpty, true); }); test('getLostData get both path and error should throw', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(picker.channel, + (MethodCall methodCall) async { return { 'type': 'video', 'errorCode': 'test_error_code', @@ -1201,8 +1243,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getImageFromSource(source: ImageSource.gallery), isNull); @@ -1431,8 +1475,10 @@ void main() { }); test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + picker.channel, (MethodCall methodCall) => null); expect(await picker.getMultiImage(), isNull); expect(await picker.getMultiImage(), isNull); @@ -1478,3 +1524,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/image_picker/image_picker_windows/CHANGELOG.md b/packages/image_picker/image_picker_windows/CHANGELOG.md index 427598760a4b..e739db71363e 100644 --- a/packages/image_picker/image_picker_windows/CHANGELOG.md +++ b/packages/image_picker/image_picker_windows/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.1.0+4 + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + ## 0.1.0+3 * Changes XTypeGroup initialization from final to const. diff --git a/packages/image_picker/image_picker_windows/example/lib/main.dart b/packages/image_picker/image_picker_windows/example/lib/main.dart index e340a185bf3d..dae45a5e2957 100644 --- a/packages/image_picker/image_picker_windows/example/lib/main.dart +++ b/packages/image_picker/image_picker_windows/example/lib/main.dart @@ -70,8 +70,8 @@ class _MyHomePageState extends State { } } - Future _handleMultiImagePicked(BuildContext? context) async { - await _displayPickImageDialog(context!, + Future _handleMultiImagePicked(BuildContext context) async { + await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { final List? pickedFileList = await _picker.pickMultiImage( @@ -91,8 +91,8 @@ class _MyHomePageState extends State { } Future _handleSingleImagePicked( - BuildContext? context, ImageSource source) async { - await _displayPickImageDialog(context!, + BuildContext context, ImageSource source) async { + await _displayPickImageDialog(context, (double? maxWidth, double? maxHeight, int? quality) async { try { final PickedFile? pickedFile = await _picker.pickImage( @@ -113,18 +113,20 @@ class _MyHomePageState extends State { } Future _onImageButtonPressed(ImageSource source, - {BuildContext? context, bool isMultiImage = false}) async { + {required BuildContext context, bool isMultiImage = false}) async { if (_controller != null) { await _controller!.setVolume(0.0); } - if (_isVideo) { - final PickedFile? file = await _picker.pickVideo( - source: source, maxDuration: const Duration(seconds: 10)); - await _playVideo(file); - } else if (isMultiImage) { - await _handleMultiImagePicked(context); - } else { - await _handleSingleImagePicked(context, source); + if (context.mounted) { + if (_isVideo) { + final PickedFile? file = await _picker.pickVideo( + source: source, maxDuration: const Duration(seconds: 10)); + await _playVideo(file); + } else if (isMultiImage) { + await _handleMultiImagePicked(context); + } else { + await _handleSingleImagePicked(context, source); + } } } @@ -269,7 +271,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { _isVideo = true; - _onImageButtonPressed(ImageSource.gallery); + _onImageButtonPressed(ImageSource.gallery, context: context); }, heroTag: 'video0', tooltip: 'Pick Video from gallery', @@ -282,7 +284,7 @@ class _MyHomePageState extends State { backgroundColor: Colors.red, onPressed: () { _isVideo = true; - _onImageButtonPressed(ImageSource.camera); + _onImageButtonPressed(ImageSource.camera, context: context); }, heroTag: 'video1', tooltip: 'Take a Video', diff --git a/packages/image_picker/image_picker_windows/example/pubspec.yaml b/packages/image_picker/image_picker_windows/example/pubspec.yaml index b87000a6caff..bdbd182d3fc5 100644 --- a/packages/image_picker/image_picker_windows/example/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/example/pubspec.yaml @@ -5,7 +5,7 @@ version: 1.0.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/image_picker/image_picker_windows/pubspec.yaml b/packages/image_picker/image_picker_windows/pubspec.yaml index 5d6988cc2931..07fa673649de 100644 --- a/packages/image_picker/image_picker_windows/pubspec.yaml +++ b/packages/image_picker/image_picker_windows/pubspec.yaml @@ -2,11 +2,11 @@ name: image_picker_windows description: Windows platform implementation of image_picker repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 -version: 0.1.0+3 +version: 0.1.0+4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md index c6d2270bbc46..19e65372662d 100644 --- a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -1,3 +1,16 @@ +## 3.1.4 + +* Updates iOS minimum version in README. + +## 3.1.3 + +* Ignores a lint in the example app for backwards compatibility. + +## 3.1.2 + +* Updates example code for `use_build_context_synchronously` lint. +* Updates minimum Flutter version to 3.0. + ## 3.1.1 * Adds screenshots to pubspec.yaml. diff --git a/packages/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md index 91ca5233a2fc..6df0ebaccaa0 100644 --- a/packages/in_app_purchase/in_app_purchase/README.md +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -5,9 +5,9 @@ A storefront-independent API for purchases in Flutter apps. This plugin supports in-app purchases (_IAP_) through an _underlying store_, which can be the App Store (on iOS and macOS) or Google Play (on Android). -| | Android | iOS | macOS | -|-------------|---------|------|--------| -| **Support** | SDK 16+ | 9.0+ | 10.15+ | +| | Android | iOS | macOS | +|-------------|---------|-------|--------| +| **Support** | SDK 16+ | 11.0+ | 10.15+ |

CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 11.0 diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Podfile b/packages/in_app_purchase/in_app_purchase/example/ios/Podfile index 310b9b498ba6..cad555de0518 100644 --- a/packages/in_app_purchase/in_app_purchase/example/ios/Podfile +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj index df13d20ae61d..8b83bba96707 100644 --- a/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -176,7 +176,7 @@ isa = PBXProject; attributes = { DefaultBuildSystemTypeForWorkspace = Original; - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -224,10 +224,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -256,6 +258,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -351,7 +354,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -401,7 +404,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3bb3697ef41c..50a8cfc99f50 100644 --- a/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/in_app_purchase/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase/example/lib/main.dart index 9e53b4bf8b8e..21268d4e7e8a 100644 --- a/packages/in_app_purchase/in_app_purchase/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase/example/lib/main.dart @@ -164,6 +164,8 @@ class _MyAppState extends State<_MyApp> { } if (_purchasePending) { stack.add( + // TODO(goderbauer): Make this const when that's available on stable. + // ignore: prefer_const_constructors Stack( children: const [ Opacity( @@ -468,17 +470,19 @@ class _MyAppState extends State<_MyApp> { await androidAddition.launchPriceChangeConfirmationFlow( sku: 'purchaseId', ); - if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Price change accepted'), - )); - } else { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text( - priceChangeConfirmationResult.debugMessage ?? - 'Price change failed with code ${priceChangeConfirmationResult.responseCode}', - ), - )); + if (context.mounted) { + if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Price change accepted'), + )); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + priceChangeConfirmationResult.debugMessage ?? + 'Price change failed with code ${priceChangeConfirmationResult.responseCode}', + ), + )); + } } } if (Platform.isIOS) { diff --git a/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml index 6d2297572cb9..8037b1a4c1ef 100644 --- a/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml index 875e50688b06..483fe2c3b691 100644 --- a/packages/in_app_purchase/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -2,11 +2,11 @@ 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. repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 3.1.1 +version: 3.1.4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md index 327fc5c0f053..b8fecc8f7e64 100644 --- a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -1,3 +1,18 @@ +## 0.2.5 + +* Fixes the management of `BillingClient` connection. +* Introduces `BillingClientManager`. + +## 0.2.4+1 + +* Updates Google Play Billing Library to 5.1.0. +* Updates androidx.annotation to 1.5.0. + +## 0.2.4 + +* Updates minimum Flutter version to 3.0. +* Ignores a lint in the example app for backwards compatibility. + ## 0.2.3+9 * Updates `androidx.test.espresso:espresso-core` to 3.5.1. diff --git a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle index 16be43200bb4..0d4bde6183cd 100644 --- a/packages/in_app_purchase/in_app_purchase_android/android/build.gradle +++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle @@ -29,10 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -54,8 +51,8 @@ android { } dependencies { - implementation 'androidx.annotation:annotation:1.3.0' - implementation 'com.android.billingclient:billing:5.0.0' + implementation 'androidx.annotation:annotation:1.5.0' + implementation 'com.android.billingclient:billing:5.1.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.json:json:20220924' testImplementation 'org.mockito:mockito-core:4.7.0' diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/build.gradle b/packages/in_app_purchase/in_app_purchase_android/example/android/app/build.gradle index 281f349989be..511091df086d 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/android/app/build.gradle +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/build.gradle @@ -108,7 +108,7 @@ flutter { dependencies { implementation 'com.android.billingclient:billing:5.0.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:5.1.1' testImplementation 'org.json:json:20220924' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/gradle.properties b/packages/in_app_purchase/in_app_purchase_android/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/android/gradle.properties +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart index cc612d1918b6..97e71b038be3 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart @@ -156,6 +156,8 @@ class _MyAppState extends State<_MyApp> { } if (_purchasePending) { stack.add( + // TODO(goderbauer): Make this const when that's available on stable. + // ignore: prefer_const_constructors Stack( children: const [ Opacity( diff --git a/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml index af760a3ada46..d5a76b848093 100644 --- a/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart b/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart index 1dac19f825b8..b49be8fe0fe1 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +export 'src/billing_client_wrappers/billing_client_manager.dart'; export 'src/billing_client_wrappers/billing_client_wrapper.dart'; export 'src/billing_client_wrappers/purchase_wrapper.dart'; export 'src/billing_client_wrappers/sku_details_wrapper.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart new file mode 100644 index 000000000000..e7ee7c90f054 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_manager.dart @@ -0,0 +1,155 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +import 'billing_client_wrapper.dart'; +import 'purchase_wrapper.dart'; + +/// Abstraction of result of [BillingClient] operation that includes +/// a [BillingResponse]. +abstract class HasBillingResponse { + /// The status of the operation. + abstract final BillingResponse responseCode; +} + +/// Utility class that manages a [BillingClient] connection. +/// +/// Connection is initialized on creation of [BillingClientManager]. +/// If [BillingClient] sends `onBillingServiceDisconnected` event or any +/// operation returns [BillingResponse.serviceDisconnected], connection is +/// re-initialized. +/// +/// [BillingClient] instance is not exposed directly. It can be accessed via +/// [withClient] and [withClientNonRetryable] methods that handle the +/// connection management. +/// +/// Consider calling [dispose] after the [BillingClient] is no longer needed. +class BillingClientManager { + /// Creates the [BillingClientManager]. + /// + /// Immediately initializes connection to the underlying [BillingClient]. + BillingClientManager() { + _connect(); + } + + /// Stream of `onPurchasesUpdated` events from the [BillingClient]. + /// + /// This is a broadcast stream, so it can be listened to multiple times. + /// A "done" event will be sent after [dispose] is called. + late final Stream purchasesUpdatedStream = + _purchasesUpdatedController.stream; + + /// [BillingClient] instance managed by this [BillingClientManager]. + /// + /// In order to access the [BillingClient], consider using [withClient] + /// and [withClientNonRetryable] + /// methods. + @visibleForTesting + late final BillingClient client = BillingClient(_onPurchasesUpdated); + + final StreamController _purchasesUpdatedController = + StreamController.broadcast(); + + bool _isConnecting = false; + bool _isDisposed = false; + + // Initialized immediately in the constructor, so it's always safe to access. + late Future _readyFuture; + + /// Executes the given [action] with access to the underlying [BillingClient]. + /// + /// If necessary, waits for the underlying [BillingClient] to connect. + /// If given [action] returns [BillingResponse.serviceDisconnected], it will + /// be transparently retried after the connection is restored. Because + /// of this, [action] may be called multiple times. + /// + /// A response with [BillingResponse.serviceDisconnected] may be returned + /// in case of [dispose] being called during the operation. + /// + /// See [withClientNonRetryable] for operations that do not return a subclass + /// of [HasBillingResponse]. + Future withClient( + Future Function(BillingClient client) action, + ) async { + _debugAssertNotDisposed(); + await _readyFuture; + final R result = await action(client); + if (result.responseCode == BillingResponse.serviceDisconnected && + !_isDisposed) { + await _connect(); + return withClient(action); + } else { + return result; + } + } + + /// Executes the given [action] with access to the underlying [BillingClient]. + /// + /// If necessary, waits for the underlying [BillingClient] to connect. + /// Designed only for operations that do not return a subclass + /// of [HasBillingResponse] (e.g. [BillingClient.isReady], + /// [BillingClient.isFeatureSupported]). + /// + /// See [withClient] for operations that return a subclass + /// of [HasBillingResponse]. + Future withClientNonRetryable( + Future Function(BillingClient client) action, + ) async { + _debugAssertNotDisposed(); + await _readyFuture; + return action(client); + } + + /// Ends connection to the [BillingClient]. + /// + /// Consider calling [dispose] after you no longer need the [BillingClient] + /// API to free up the resources. + /// + /// After calling [dispose] : + /// - Further connection attempts will not be made; + /// - [purchasesUpdatedStream] will be closed; + /// - Calls to [withClient] and [withClientNonRetryable] will throw. + void dispose() { + _debugAssertNotDisposed(); + _isDisposed = true; + client.endConnection(); + _purchasesUpdatedController.close(); + } + + // If disposed, does nothing. + // If currently connecting, waits for it to complete. + // Otherwise, starts a new connection. + Future _connect() { + if (_isDisposed) { + return Future.value(); + } + if (_isConnecting) { + return _readyFuture; + } + _isConnecting = true; + _readyFuture = Future.sync(() async { + await client.startConnection(onBillingServiceDisconnected: _connect); + _isConnecting = false; + }); + return _readyFuture; + } + + void _onPurchasesUpdated(PurchasesResultWrapper event) { + if (_isDisposed) { + return; + } + _purchasesUpdatedController.add(event); + } + + void _debugAssertNotDisposed() { + assert( + !_isDisposed, + 'A BillingClientManager was used after being disposed. Once you have ' + 'called dispose() on a BillingClientManager, it can no longer be used.', + ); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart index 2d4a3f96b50e..04a73f6c5645 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart @@ -51,6 +51,12 @@ typedef PurchasesUpdatedListener = void Function( /// `com.android.billingclient.api.BillingClient` API as much as possible, with /// some minor changes to account for language differences. Callbacks have been /// converted to futures where appropriate. +/// +/// Connection to [BillingClient] may be lost at any time (see +/// `onBillingServiceDisconnected` param of [startConnection] and +/// [BillingResponse.serviceDisconnected]). +/// Consider using [BillingClientManager] that handles these disconnections +/// transparently. class BillingClient { /// Creates a billing client. BillingClient(PurchasesUpdatedListener onPurchasesUpdated) { diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart index 4e6b953096e2..633aa732165b 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'billing_client_manager.dart'; import 'billing_client_wrapper.dart'; import 'sku_details_wrapper.dart'; @@ -265,7 +266,7 @@ class PurchaseHistoryRecordWrapper { @JsonSerializable() @BillingResponseConverter() @immutable -class PurchasesResultWrapper { +class PurchasesResultWrapper implements HasBillingResponse { /// Creates a [PurchasesResultWrapper] with the given purchase result details. const PurchasesResultWrapper( {required this.responseCode, @@ -300,6 +301,7 @@ class PurchasesResultWrapper { /// /// This can represent either the status of the "query purchase history" half /// of the operation and the "user made purchases" transaction itself. + @override final BillingResponse responseCode; /// The list of successful purchases made in this transaction. @@ -316,7 +318,7 @@ class PurchasesResultWrapper { @JsonSerializable() @BillingResponseConverter() @immutable -class PurchasesHistoryResult { +class PurchasesHistoryResult implements HasBillingResponse { /// Creates a [PurchasesHistoryResult] with the provided history. const PurchasesHistoryResult( {required this.billingResult, required this.purchaseHistoryRecordList}); @@ -325,6 +327,9 @@ class PurchasesHistoryResult { factory PurchasesHistoryResult.fromJson(Map map) => _$PurchasesHistoryResultFromJson(map); + @override + BillingResponse get responseCode => billingResult.responseCode; + @override bool operator ==(Object other) { if (identical(other, this)) { diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart index 1c5c2d1fcee9..2689cf37eac4 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'billing_client_manager.dart'; import 'billing_client_wrapper.dart'; // WARNING: Changes to `@JsonSerializable` classes need to be reflected in the @@ -182,7 +183,7 @@ class SkuDetailsWrapper { /// Returned by [BillingClient.querySkuDetails]. @JsonSerializable() @immutable -class SkuDetailsResponseWrapper { +class SkuDetailsResponseWrapper implements HasBillingResponse { /// Creates a [SkuDetailsResponseWrapper] with the given purchase details. @visibleForTesting const SkuDetailsResponseWrapper( @@ -202,6 +203,9 @@ class SkuDetailsResponseWrapper { @JsonKey(defaultValue: []) final List skuDetailsList; + @override + BillingResponse get responseCode => billingResult.responseCode; + @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) { @@ -221,7 +225,7 @@ class SkuDetailsResponseWrapper { @JsonSerializable() @BillingResponseConverter() @immutable -class BillingResultWrapper { +class BillingResultWrapper implements HasBillingResponse { /// Constructs the object with [responseCode] and [debugMessage]. const BillingResultWrapper({required this.responseCode, this.debugMessage}); @@ -239,6 +243,7 @@ class BillingResultWrapper { } /// Response code returned in the Play Billing API calls. + @override final BillingResponse responseCode; /// Debug message returned in the Play Billing API calls. diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart index c8046d6e655a..57d6850d9e93 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart @@ -29,18 +29,13 @@ const String kIAPSource = 'google_play'; /// generic plugin API. class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { InAppPurchaseAndroidPlatform._() { - billingClient = BillingClient((PurchasesResultWrapper resultWrapper) async { - _purchaseUpdatedController - .add(await _getPurchaseDetailsFromResult(resultWrapper)); - }); - // Register [InAppPurchaseAndroidPlatformAddition]. InAppPurchasePlatformAddition.instance = - InAppPurchaseAndroidPlatformAddition(billingClient); + InAppPurchaseAndroidPlatformAddition(billingClientManager); - _readyFuture = _connect(); - _purchaseUpdatedController = - StreamController>.broadcast(); + billingClientManager.purchasesUpdatedStream + .asyncMap(_getPurchaseDetailsFromResult) + .listen(_purchaseUpdatedController.add); } /// Registers this class as the default instance of [InAppPurchasePlatform]. @@ -50,26 +45,25 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { InAppPurchasePlatform.instance = InAppPurchaseAndroidPlatform._(); } - static late StreamController> - _purchaseUpdatedController; + final StreamController> _purchaseUpdatedController = + StreamController>.broadcast(); @override - Stream> get purchaseStream => + late final Stream> purchaseStream = _purchaseUpdatedController.stream; /// The [BillingClient] that's abstracted by [GooglePlayConnection]. /// /// This field should not be used out of test code. @visibleForTesting - late final BillingClient billingClient; + final BillingClientManager billingClientManager = BillingClientManager(); - late Future _readyFuture; static final Set _productIdsToConsume = {}; @override Future isAvailable() async { - await _readyFuture; - return billingClient.isReady(); + return billingClientManager + .withClientNonRetryable((BillingClient client) => client.isReady()); } @override @@ -77,27 +71,33 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { Set identifiers) async { List responses; PlatformException? exception; + + Future querySkuDetails(SkuType type) { + return billingClientManager.withClient( + (BillingClient client) => client.querySkuDetails( + skuType: type, + skusList: identifiers.toList(), + ), + ); + } + try { responses = await Future.wait(>[ - billingClient.querySkuDetails( - skuType: SkuType.inapp, skusList: identifiers.toList()), - billingClient.querySkuDetails( - skuType: SkuType.subs, skusList: identifiers.toList()) + querySkuDetails(SkuType.inapp), + querySkuDetails(SkuType.subs), ]); } on PlatformException catch (e) { exception = e; - responses = [ - // ignore: invalid_use_of_visible_for_testing_member - SkuDetailsResponseWrapper( - billingResult: BillingResultWrapper( - responseCode: BillingResponse.error, debugMessage: e.code), - skuDetailsList: const []), - // ignore: invalid_use_of_visible_for_testing_member - SkuDetailsResponseWrapper( - billingResult: BillingResultWrapper( - responseCode: BillingResponse.error, debugMessage: e.code), - skuDetailsList: const []) - ]; + // ignore: invalid_use_of_visible_for_testing_member + final SkuDetailsResponseWrapper response = SkuDetailsResponseWrapper( + billingResult: BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: e.code, + ), + skuDetailsList: const [], + ); + // Error response for both queries should be the same, so we can reuse it. + responses = [response, response]; } final List productDetailsList = responses.expand((SkuDetailsResponseWrapper response) { @@ -132,13 +132,16 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } final BillingResultWrapper billingResultWrapper = - await billingClient.launchBillingFlow( - sku: purchaseParam.productDetails.id, - accountId: purchaseParam.applicationUserName, - oldSku: changeSubscriptionParam?.oldPurchaseDetails.productID, - purchaseToken: changeSubscriptionParam - ?.oldPurchaseDetails.verificationData.serverVerificationData, - prorationMode: changeSubscriptionParam?.prorationMode); + await billingClientManager.withClient( + (BillingClient client) => client.launchBillingFlow( + sku: purchaseParam.productDetails.id, + accountId: purchaseParam.applicationUserName, + oldSku: changeSubscriptionParam?.oldPurchaseDetails.productID, + purchaseToken: changeSubscriptionParam + ?.oldPurchaseDetails.verificationData.serverVerificationData, + prorationMode: changeSubscriptionParam?.prorationMode, + ), + ); return billingResultWrapper.responseCode == BillingResponse.ok; } @@ -171,8 +174,10 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { 'completePurchase unsuccessful. The `purchase.verificationData` is not valid'); } - return billingClient - .acknowledgePurchase(purchase.verificationData.serverVerificationData); + return billingClientManager.withClient( + (BillingClient client) => client.acknowledgePurchase( + purchase.verificationData.serverVerificationData), + ); } @override @@ -182,8 +187,10 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { List responses; responses = await Future.wait(>[ - billingClient.queryPurchases(SkuType.inapp), - billingClient.queryPurchases(SkuType.subs) + billingClientManager + .withClient((BillingClient client) => client.queryPurchases(SkuType.inapp)), + billingClientManager + .withClient((BillingClient client) => client.queryPurchases(SkuType.subs)), ]); final Set errorCodeSet = responses @@ -219,11 +226,9 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { _purchaseUpdatedController.add(pastPurchases); } - Future _connect() => - billingClient.startConnection(onBillingServiceDisconnected: () {}); - Future _maybeAutoConsumePurchase( - PurchaseDetails purchaseDetails) async { + PurchaseDetails purchaseDetails, + ) async { if (!(purchaseDetails.status == PurchaseStatus.purchased && _productIdsToConsume.contains(purchaseDetails.productID))) { return purchaseDetails; @@ -279,15 +284,16 @@ class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { } return [ PurchaseDetails( - purchaseID: '', - productID: '', - status: status, - transactionDate: null, - verificationData: PurchaseVerificationData( - localVerificationData: '', - serverVerificationData: '', - source: kIAPSource)) - ..error = error + purchaseID: '', + productID: '', + status: status, + transactionDate: null, + verificationData: PurchaseVerificationData( + localVerificationData: '', + serverVerificationData: '', + source: kIAPSource, + ), + )..error = error ]; } } diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart index d5657d1a38d8..255071f0c0bf 100644 --- a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart @@ -12,8 +12,8 @@ import '../in_app_purchase_android.dart'; class InAppPurchaseAndroidPlatformAddition extends InAppPurchasePlatformAddition { /// Creates a [InAppPurchaseAndroidPlatformAddition] which uses the supplied - /// `BillingClient` to provide Android specific features. - InAppPurchaseAndroidPlatformAddition(this._billingClient); + /// `BillingClientManager` to provide Android specific features. + InAppPurchaseAndroidPlatformAddition(this._billingClientManager); /// Whether pending purchase is enabled. /// @@ -42,7 +42,7 @@ class InAppPurchaseAndroidPlatformAddition // No-op, until it is time to completely remove this method from the API. } - final BillingClient _billingClient; + final BillingClientManager _billingClientManager; /// Mark that the user has consumed a product. /// @@ -54,8 +54,10 @@ class InAppPurchaseAndroidPlatformAddition throw ArgumentError( 'consumePurchase unsuccessful. The `purchase.verificationData` is not valid'); } - return _billingClient - .consumeAsync(purchase.verificationData.serverVerificationData); + return _billingClientManager.withClient( + (BillingClient client) => + client.consumeAsync(purchase.verificationData.serverVerificationData), + ); } /// Query all previous purchases. @@ -78,8 +80,12 @@ class InAppPurchaseAndroidPlatformAddition PlatformException? exception; try { responses = await Future.wait(>[ - _billingClient.queryPurchases(SkuType.inapp), - _billingClient.queryPurchases(SkuType.subs) + _billingClientManager.withClient( + (BillingClient client) => client.queryPurchases(SkuType.inapp), + ), + _billingClientManager.withClient( + (BillingClient client) => client.queryPurchases(SkuType.subs), + ), ]); } on PlatformException catch (e) { exception = e; @@ -141,7 +147,8 @@ class InAppPurchaseAndroidPlatformAddition /// Checks if the specified feature or capability is supported by the Play Store. /// Call this to check if a [BillingClientFeature] is supported by the device. Future isFeatureSupported(BillingClientFeature feature) async { - return _billingClient.isFeatureSupported(feature); + return _billingClientManager + .withClientNonRetryable((BillingClient client) => client.isFeatureSupported(feature)); } /// Initiates a flow to confirm the change of price for an item subscribed by the user. @@ -153,6 +160,9 @@ class InAppPurchaseAndroidPlatformAddition /// [InAppPurchaseAndroidPlatform.queryProductDetails] call. Future launchPriceChangeConfirmationFlow( {required String sku}) { - return _billingClient.launchPriceChangeConfirmationFlow(sku: sku); + return _billingClientManager.withClient( + (BillingClient client) => + client.launchPriceChangeConfirmationFlow(sku: sku), + ); } } diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml index 332cf850af04..ffb55e947b34 100644 --- a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -2,11 +2,11 @@ name: in_app_purchase_android description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs. repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.2.3+9 +version: 0.2.5 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart new file mode 100644 index 000000000000..bd64fabe8002 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_manager_test.dart @@ -0,0 +1,107 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; +import 'package:in_app_purchase_android/src/channel.dart'; + +import '../stub_in_app_purchase_platform.dart'; +import 'purchase_wrapper_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); + late BillingClientManager manager; + late Completer connectedCompleter; + + const String startConnectionCall = + 'BillingClient#startConnection(BillingClientStateListener)'; + const String endConnectionCall = 'BillingClient#endConnection()'; + const String onBillingServiceDisconnectedCallback = + 'BillingClientStateListener#onBillingServiceDisconnected()'; + + setUpAll(() => + channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler)); + + setUp(() { + WidgetsFlutterBinding.ensureInitialized(); + connectedCompleter = Completer.sync(); + stubPlatform.addResponse( + name: startConnectionCall, + value: buildBillingResultMap( + const BillingResultWrapper(responseCode: BillingResponse.ok), + ), + additionalStepBeforeReturn: (dynamic _) => connectedCompleter.future, + ); + stubPlatform.addResponse(name: endConnectionCall); + manager = BillingClientManager(); + }); + + tearDown(() => stubPlatform.reset()); + + group('BillingClientWrapper', () { + test('connects on initialization', () { + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); + }); + + test('waits for connection before executing the operations', () { + bool called1 = false; + bool called2 = false; + manager.withClient((BillingClient _) async { + called1 = true; + return const BillingResultWrapper(responseCode: BillingResponse.ok); + }); + manager.withClientNonRetryable( + (BillingClient _) async => called2 = true, + ); + expect(called1, equals(false)); + expect(called2, equals(false)); + connectedCompleter.complete(); + expect(called1, equals(true)); + expect(called2, equals(true)); + }); + + test('re-connects when client sends onBillingServiceDisconnected', () { + connectedCompleter.complete(); + manager.client.callHandler( + const MethodCall(onBillingServiceDisconnectedCallback, + {'handle': 0}), + ); + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + }); + + test( + 're-connects when operation returns BillingResponse.serviceDisconnected', + () async { + connectedCompleter.complete(); + int timesCalled = 0; + final BillingResultWrapper result = await manager.withClient( + (BillingClient _) async { + timesCalled++; + return BillingResultWrapper( + responseCode: timesCalled == 1 + ? BillingResponse.serviceDisconnected + : BillingResponse.ok, + ); + }, + ); + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + expect(timesCalled, equals(2)); + expect(result.responseCode, equals(BillingResponse.ok)); + }, + ); + + test('does not re-connect when disposed', () { + connectedCompleter.complete(); + manager.dispose(); + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); + expect(stubPlatform.countPreviousCalls(endConnectionCall), equals(1)); + }); + }); +} diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart index 4dae957e21eb..98219dc9d4e5 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -17,8 +17,9 @@ void main() { final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); late BillingClient billingClient; - setUpAll(() => - channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler)); + setUpAll(() => _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler)); setUp(() { billingClient = BillingClient((PurchasesResultWrapper _) {}); @@ -651,3 +652,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart index 9737282e27b7..1b61f53b0d38 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart @@ -22,7 +22,9 @@ void main() { const String endConnectionCall = 'BillingClient#endConnection()'; setUpAll(() { - channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler); }); setUp(() { @@ -37,7 +39,7 @@ void main() { value: buildBillingResultMap(expectedBillingResult)); stubPlatform.addResponse(name: endConnectionCall); iapAndroidPlatformAddition = - InAppPurchaseAndroidPlatformAddition(BillingClient((_) {})); + InAppPurchaseAndroidPlatformAddition(BillingClientManager()); }); group('consume purchases', () { @@ -213,3 +215,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart index 70e519ce9f6e..a679def27d51 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart @@ -24,9 +24,15 @@ void main() { const String startConnectionCall = 'BillingClient#startConnection(BillingClientStateListener)'; const String endConnectionCall = 'BillingClient#endConnection()'; + const String acknowledgePurchaseCall = + 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; + const String onBillingServiceDisconnectedCallback = + 'BillingClientStateListener#onBillingServiceDisconnected()'; setUpAll(() { - channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler); }); setUp(() { @@ -55,6 +61,45 @@ void main() { //await iapAndroidPlatform.isAvailable(); expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); }); + + test('re-connects when client sends onBillingServiceDisconnected', () { + iapAndroidPlatform.billingClientManager.client.callHandler( + const MethodCall(onBillingServiceDisconnectedCallback, + {'handle': 0}), + ); + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + }); + + test( + 're-connects when operation returns BillingResponse.clientDisconnected', + () async { + final Map okValue = buildBillingResultMap( + const BillingResultWrapper(responseCode: BillingResponse.ok)); + stubPlatform.addResponse( + name: acknowledgePurchaseCall, + value: buildBillingResultMap( + const BillingResultWrapper( + responseCode: BillingResponse.serviceDisconnected, + ), + ), + ); + stubPlatform.addResponse( + name: startConnectionCall, + value: okValue, + additionalStepBeforeReturn: (dynamic _) => stubPlatform.addResponse( + name: acknowledgePurchaseCall, value: okValue), + ); + final PurchaseDetails purchase = + GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase); + final BillingResultWrapper result = + await iapAndroidPlatform.completePurchase(purchase); + expect( + stubPlatform.countPreviousCalls(acknowledgePurchaseCall), + equals(2), + ); + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(2)); + expect(result.responseCode, equals(BillingResponse.ok)); + }); }); group('isAvailable', () { @@ -312,7 +357,7 @@ void main() { } ] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; @@ -356,7 +401,7 @@ void main() { 'responseCode': const BillingResponseConverter().toJson(sentCode), 'purchasesList': const [] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer completer = Completer(); PurchaseDetails purchaseDetails; @@ -414,7 +459,7 @@ void main() { } ] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase @@ -529,7 +574,7 @@ void main() { } ] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase @@ -607,7 +652,7 @@ void main() { } ] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase @@ -673,7 +718,7 @@ void main() { } ] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer consumeCompleter = Completer(); // adding call back for consume purchase @@ -731,7 +776,7 @@ void main() { 'responseCode': const BillingResponseConverter().toJson(sentCode), 'purchasesList': const [] }); - iapAndroidPlatform.billingClient.callHandler(call); + iapAndroidPlatform.billingClientManager.client.callHandler(call); }); final Completer completer = Completer(); @@ -786,3 +831,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart b/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart index 75972e644faa..35e2807bc3b1 100644 --- a/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.dart @@ -5,7 +5,9 @@ import 'dart:async'; import 'package:flutter/services.dart'; -typedef AdditionalSteps = void Function(dynamic args); +// `FutureOr` instead of `FutureOr` to avoid +// "don't assign to void" warnings. +typedef AdditionalSteps = FutureOr Function(dynamic args); class StubInAppPurchasePlatform { final Map _expectedCalls = {}; @@ -36,7 +38,7 @@ class StubInAppPurchasePlatform { _previousCalls.add(call); if (_expectedCalls.containsKey(call.method)) { if (_additionalSteps[call.method] != null) { - _additionalSteps[call.method]!(call.arguments); + await _additionalSteps[call.method]!(call.arguments); } return Future.sync(() => _expectedCalls[call.method]); } else { diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md index 17ba02986088..a408c2db2cd7 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 1.3.2 * Updates imports for `prefer_relative_imports`. diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml index 46e38b0a03fa..b3420161530b 100644 --- a/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 1.3.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md index 434caf425d00..6314bdc323f5 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md +++ b/packages/in_app_purchase/in_app_purchase_storekit/CHANGELOG.md @@ -1,3 +1,20 @@ +## 0.3.6 + +* Updates minimum Flutter version to 3.3 and iOS 11. + +## 0.3.5+2 + +* Fix a crash when `appStoreReceiptURL` is nil. + +## 0.3.5+1 + +* Uses the new `sharedDarwinSource` flag when available. + +## 0.3.5 + +* Updates minimum Flutter version to 3.0. +* Ignores a lint in the example app for backwards compatibility. + ## 0.3.4+1 * Updates code for stricter lint checks. @@ -8,7 +25,7 @@ ## 0.3.3 -* Supports adding discount information to AppStorePurchaseParam. +* Supports adding discount information to AppStorePurchaseParam. * Fixes iOS Promotional Offers bug which prevents them from working. ## 0.3.2+2 diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.h similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAObjectTranslator.h rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.h diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.m similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAObjectTranslator.m rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAObjectTranslator.m diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPPaymentQueueDelegate.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPPaymentQueueDelegate.h similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPPaymentQueueDelegate.h rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPPaymentQueueDelegate.h diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPPaymentQueueDelegate.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPPaymentQueueDelegate.m similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPPaymentQueueDelegate.m rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPPaymentQueueDelegate.m diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPReceiptManager.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.h similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPReceiptManager.h rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.h diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.m similarity index 97% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPReceiptManager.m rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.m index fc125da133d4..320e6072d046 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPReceiptManager.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPReceiptManager.m @@ -21,6 +21,9 @@ @implementation FIAPReceiptManager - (NSString *)retrieveReceiptWithError:(FlutterError **)flutterError { NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; + if (!receiptURL) { + return nil; + } NSError *receiptError; NSData *receipt = [self getReceiptData:receiptURL error:&receiptError]; if (!receipt || receiptError) { diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPRequestHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPRequestHandler.h similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPRequestHandler.h rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPRequestHandler.h diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPRequestHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPRequestHandler.m similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPRequestHandler.m rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPRequestHandler.m diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPaymentQueueHandler.h similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPaymentQueueHandler.h rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPaymentQueueHandler.h diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPaymentQueueHandler.m similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIAPaymentQueueHandler.m rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIAPaymentQueueHandler.m diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIATransactionCache.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIATransactionCache.h similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIATransactionCache.h rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIATransactionCache.h diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIATransactionCache.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIATransactionCache.m similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/FIATransactionCache.m rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/FIATransactionCache.m diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.h similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/InAppPurchasePlugin.h rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.h diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.m similarity index 100% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/Classes/InAppPurchasePlugin.m rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/Classes/InAppPurchasePlugin.m diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/in_app_purchase_storekit.podspec b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit.podspec similarity index 97% rename from packages/in_app_purchase/in_app_purchase_storekit/shared/in_app_purchase_storekit.podspec rename to packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit.podspec index 84c385e3405c..57a24bd674ab 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/shared/in_app_purchase_storekit.podspec +++ b/packages/in_app_purchase/in_app_purchase_storekit/darwin/in_app_purchase_storekit.podspec @@ -20,7 +20,7 @@ Downloaded by pub (not CocoaPods). s.public_header_files = 'Classes/**/*.h' s.ios.dependency 'Flutter' s.osx.dependency 'FlutterMacOS' - s.ios.deployment_target = '9.0' + s.ios.deployment_target = '11.0' s.osx.deployment_target = '10.15' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/AppFrameworkInfo.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/AppFrameworkInfo.plist index 8d4492f977ad..9625e105df39 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Podfile b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Podfile index 5200b9fa5045..4f563887c820 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Podfile +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj index 3977d549af12..4b24d767a226 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -318,10 +318,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -354,6 +356,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -493,7 +496,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -543,7 +546,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist index a8f31ba92572..3c493732947a 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/ios/Runner/Info.plist @@ -41,5 +41,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart b/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart index 09058ea2e89a..ce06aa1d1ab6 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/lib/main.dart @@ -156,6 +156,8 @@ class _MyAppState extends State<_MyApp> { } if (_purchasePending) { stack.add( + // TODO(goderbauer): Make this const when that's available on stable. + // ignore: prefer_const_constructors Stack( children: const [ Opacity( diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml index e71b85d4b447..b06dd6a9a594 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.m index 9ace425ce1dc..f7e6dcdaab16 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/InAppPurchasePluginTests.m @@ -313,6 +313,23 @@ - (void)testRetrieveReceiptDataSuccess { XCTAssert([result isKindOfClass:[NSString class]]); } +- (void)testRetrieveReceiptDataNil { + NSBundle *mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub(mockBundle.appStoreReceiptURL).andReturn(nil); + XCTestExpectation *expectation = [self expectationWithDescription:@"nil receipt data retrieved"]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; + __block NSDictionary *result; + [self.plugin handleMethodCall:call + result:^(id r) { + result = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNil(result); +} + - (void)testRetrieveReceiptDataError { XCTestExpectation *expectation = [self expectationWithDescription:@"receipt data retrieved"]; FlutterMethodCall *call = [FlutterMethodCall diff --git a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/TranslatorTests.m index 34d686753762..6f77fa72a632 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/TranslatorTests.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/example/shared/RunnerTests/TranslatorTests.m @@ -224,12 +224,10 @@ - (void)testErrorWithUnsupportedUserInfo { } - (void)testLocaleToMap { - if (@available(iOS 10.0, *)) { - NSLocale *system = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; - NSDictionary *map = [FIAObjectTranslator getMapFromNSLocale:system]; - XCTAssertEqualObjects(map[@"currencySymbol"], system.currencySymbol); - XCTAssertEqualObjects(map[@"countryCode"], system.countryCode); - } + NSLocale *system = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; + NSDictionary *map = [FIAObjectTranslator getMapFromNSLocale:system]; + XCTAssertEqualObjects(map[@"currencySymbol"], system.currencySymbol); + XCTAssertEqualObjects(map[@"countryCode"], system.countryCode); } - (void)testSKStorefrontToMap { diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Assets/.gitkeep b/packages/in_app_purchase/in_app_purchase_storekit/ios/Assets/.gitkeep deleted file mode 120000 index bf2007784034..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Assets/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -../../shared/Assets/.gitkeep \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h index 8c80f07ea9a6..6b974bc7d268 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.h @@ -1 +1 @@ -../../shared/Classes/FIAObjectTranslator.h \ No newline at end of file +../../darwin/Classes/FIAObjectTranslator.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m index 643df24599b8..f9b4ffe6732d 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAObjectTranslator.m @@ -1 +1 @@ -../../shared/Classes/FIAObjectTranslator.m \ No newline at end of file +../../darwin/Classes/FIAObjectTranslator.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h index 5e54d74d187a..e4b452397bc2 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.h @@ -1 +1 @@ -../../shared/Classes/FIAPPaymentQueueDelegate.h \ No newline at end of file +../../darwin/Classes/FIAPPaymentQueueDelegate.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m index f972e7d7c7e8..a1b95ef97c1b 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPPaymentQueueDelegate.m @@ -1 +1 @@ -../../shared/Classes/FIAPPaymentQueueDelegate.m \ No newline at end of file +../../darwin/Classes/FIAPPaymentQueueDelegate.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h index f5c64da51bf3..88f02af0b00a 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.h @@ -1 +1 @@ -../../shared/Classes/FIAPReceiptManager.h \ No newline at end of file +../../darwin/Classes/FIAPReceiptManager.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m index 7cc0593abb34..f303c3c162a0 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPReceiptManager.m @@ -1 +1 @@ -../../shared/Classes/FIAPReceiptManager.m \ No newline at end of file +../../darwin/Classes/FIAPReceiptManager.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h index b008c38df4bb..9eb31f26b048 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.h @@ -1 +1 @@ -../../shared/Classes/FIAPRequestHandler.h \ No newline at end of file +../../darwin/Classes/FIAPRequestHandler.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m index 22a1ba3a7c48..d6976dc0dd26 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPRequestHandler.m @@ -1 +1 @@ -../../shared/Classes/FIAPRequestHandler.m \ No newline at end of file +../../darwin/Classes/FIAPRequestHandler.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h index 8a64356be52e..6bc9c2f6dc85 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.h @@ -1 +1 @@ -../../shared/Classes/FIAPaymentQueueHandler.h \ No newline at end of file +../../darwin/Classes/FIAPaymentQueueHandler.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m index 87359d2e1c55..8c892d29f1e6 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIAPaymentQueueHandler.m @@ -1 +1 @@ -../../shared/Classes/FIAPaymentQueueHandler.m \ No newline at end of file +../../darwin/Classes/FIAPaymentQueueHandler.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h index 1f8f3f92da93..8862d80dde39 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.h @@ -1 +1 @@ -../../shared/Classes/FIATransactionCache.h \ No newline at end of file +../../darwin/Classes/FIATransactionCache.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m index b27e9811319e..8c0dd87c7e97 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/FIATransactionCache.m @@ -1 +1 @@ -../../shared/Classes/FIATransactionCache.m \ No newline at end of file +../../darwin/Classes/FIATransactionCache.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h index d92777687ecd..0ec6c66d54f8 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.h @@ -1 +1 @@ -../../shared/Classes/InAppPurchasePlugin.h \ No newline at end of file +../../darwin/Classes/InAppPurchasePlugin.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m index 67f61aad1fb0..e087d55187e8 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/Classes/InAppPurchasePlugin.m @@ -1 +1 @@ -../../shared/Classes/InAppPurchasePlugin.m \ No newline at end of file +../../darwin/Classes/InAppPurchasePlugin.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec b/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec index 79982cb307de..4157364db8d6 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec +++ b/packages/in_app_purchase/in_app_purchase_storekit/ios/in_app_purchase_storekit.podspec @@ -1 +1 @@ -../shared/in_app_purchase_storekit.podspec \ No newline at end of file +../darwin/in_app_purchase_storekit.podspec \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_product_details.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_product_details.dart index a5d8c7287e3c..47bcf616fa40 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_product_details.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_product_details.dart @@ -12,23 +12,15 @@ class AppStoreProductDetails extends ProductDetails { /// Creates a new AppStore specific product details object with the provided /// details. AppStoreProductDetails({ - required String id, - required String title, - required String description, - required String price, - required double rawPrice, - required String currencyCode, + required super.id, + required super.title, + required super.description, + required super.price, + required super.rawPrice, + required super.currencyCode, required this.skProduct, - required String currencySymbol, - }) : super( - id: id, - title: title, - description: description, - price: price, - rawPrice: rawPrice, - currencyCode: currencyCode, - currencySymbol: currencySymbol, - ); + required super.currencySymbol, + }); /// Generate a [AppStoreProductDetails] object based on an iOS [SKProductWrapper] object. factory AppStoreProductDetails.fromSKProduct(SKProductWrapper product) { diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart index 42cb225ede0a..21a1e11116b7 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_details.dart @@ -13,19 +13,14 @@ import '../store_kit_wrappers/enum_converters.dart'; class AppStorePurchaseDetails extends PurchaseDetails { /// Creates a new AppStore specific purchase details object with the provided /// details. - AppStorePurchaseDetails( - {String? purchaseID, - required String productID, - required PurchaseVerificationData verificationData, - required String? transactionDate, - required this.skPaymentTransaction, - required PurchaseStatus status}) - : super( - productID: productID, - purchaseID: purchaseID, - transactionDate: transactionDate, - verificationData: verificationData, - status: status) { + AppStorePurchaseDetails({ + super.purchaseID, + required super.productID, + required super.verificationData, + required super.transactionDate, + required this.skPaymentTransaction, + required PurchaseStatus status, + }) : super(status: status) { this.status = status; } diff --git a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart index 0e7e24166c4d..05096d3be40e 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/lib/src/types/app_store_purchase_param.dart @@ -10,15 +10,12 @@ import '../../store_kit_wrappers.dart'; class AppStorePurchaseParam extends PurchaseParam { /// Creates a new [AppStorePurchaseParam] object with the given data. AppStorePurchaseParam({ - required ProductDetails productDetails, - String? applicationUserName, + required super.productDetails, + super.applicationUserName, this.quantity = 1, this.simulatesAskToBuyInSandbox = false, this.discount, - }) : super( - productDetails: productDetails, - applicationUserName: applicationUserName, - ); + }); /// Set it to `true` to produce an "ask to buy" flow for this payment in the /// sandbox. diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Assets/.gitkeep b/packages/in_app_purchase/in_app_purchase_storekit/macos/Assets/.gitkeep deleted file mode 120000 index bf2007784034..000000000000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Assets/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -../../shared/Assets/.gitkeep \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.h index 8c80f07ea9a6..6b974bc7d268 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.h @@ -1 +1 @@ -../../shared/Classes/FIAObjectTranslator.h \ No newline at end of file +../../darwin/Classes/FIAObjectTranslator.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.m index 643df24599b8..f9b4ffe6732d 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAObjectTranslator.m @@ -1 +1 @@ -../../shared/Classes/FIAObjectTranslator.m \ No newline at end of file +../../darwin/Classes/FIAObjectTranslator.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.h index 5e54d74d187a..e4b452397bc2 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.h @@ -1 +1 @@ -../../shared/Classes/FIAPPaymentQueueDelegate.h \ No newline at end of file +../../darwin/Classes/FIAPPaymentQueueDelegate.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.m index f972e7d7c7e8..a1b95ef97c1b 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPPaymentQueueDelegate.m @@ -1 +1 @@ -../../shared/Classes/FIAPPaymentQueueDelegate.m \ No newline at end of file +../../darwin/Classes/FIAPPaymentQueueDelegate.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.h index f5c64da51bf3..88f02af0b00a 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.h @@ -1 +1 @@ -../../shared/Classes/FIAPReceiptManager.h \ No newline at end of file +../../darwin/Classes/FIAPReceiptManager.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.m index 7cc0593abb34..f303c3c162a0 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPReceiptManager.m @@ -1 +1 @@ -../../shared/Classes/FIAPReceiptManager.m \ No newline at end of file +../../darwin/Classes/FIAPReceiptManager.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.h index b008c38df4bb..9eb31f26b048 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.h @@ -1 +1 @@ -../../shared/Classes/FIAPRequestHandler.h \ No newline at end of file +../../darwin/Classes/FIAPRequestHandler.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.m index 22a1ba3a7c48..d6976dc0dd26 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPRequestHandler.m @@ -1 +1 @@ -../../shared/Classes/FIAPRequestHandler.m \ No newline at end of file +../../darwin/Classes/FIAPRequestHandler.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.h index 8a64356be52e..6bc9c2f6dc85 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.h @@ -1 +1 @@ -../../shared/Classes/FIAPaymentQueueHandler.h \ No newline at end of file +../../darwin/Classes/FIAPaymentQueueHandler.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.m index 87359d2e1c55..8c892d29f1e6 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIAPaymentQueueHandler.m @@ -1 +1 @@ -../../shared/Classes/FIAPaymentQueueHandler.m \ No newline at end of file +../../darwin/Classes/FIAPaymentQueueHandler.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.h index 1f8f3f92da93..8862d80dde39 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.h @@ -1 +1 @@ -../../shared/Classes/FIATransactionCache.h \ No newline at end of file +../../darwin/Classes/FIATransactionCache.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.m index b27e9811319e..8c0dd87c7e97 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/FIATransactionCache.m @@ -1 +1 @@ -../../shared/Classes/FIATransactionCache.m \ No newline at end of file +../../darwin/Classes/FIATransactionCache.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.h index d92777687ecd..0ec6c66d54f8 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.h +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.h @@ -1 +1 @@ -../../shared/Classes/InAppPurchasePlugin.h \ No newline at end of file +../../darwin/Classes/InAppPurchasePlugin.h \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.m index 67f61aad1fb0..e087d55187e8 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/Classes/InAppPurchasePlugin.m @@ -1 +1 @@ -../../shared/Classes/InAppPurchasePlugin.m \ No newline at end of file +../../darwin/Classes/InAppPurchasePlugin.m \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/macos/in_app_purchase_storekit.podspec b/packages/in_app_purchase/in_app_purchase_storekit/macos/in_app_purchase_storekit.podspec index 79982cb307de..4157364db8d6 120000 --- a/packages/in_app_purchase/in_app_purchase_storekit/macos/in_app_purchase_storekit.podspec +++ b/packages/in_app_purchase/in_app_purchase_storekit/macos/in_app_purchase_storekit.podspec @@ -1 +1 @@ -../shared/in_app_purchase_storekit.podspec \ No newline at end of file +../darwin/in_app_purchase_storekit.podspec \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml index 339d12320a18..5b734f4b630c 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml +++ b/packages/in_app_purchase/in_app_purchase_storekit/pubspec.yaml @@ -2,11 +2,11 @@ name: in_app_purchase_storekit description: An implementation for the iOS and macOS platforms of the Flutter `in_app_purchase` plugin. This uses the StoreKit Framework. repository: https://github.com/flutter/plugins/tree/main/packages/in_app_purchase/in_app_purchase_storekit issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 -version: 0.3.4+1 +version: 0.3.6 environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + sdk: '>=2.18.0 <3.0.0' + flutter: ">=3.3.0" flutter: plugin: @@ -14,8 +14,10 @@ flutter: platforms: ios: pluginClass: InAppPurchasePlugin + sharedDarwinSource: true macos: pluginClass: InAppPurchasePlugin + sharedDarwinSource: true dependencies: collection: ^1.15.0 diff --git a/packages/in_app_purchase/in_app_purchase_storekit/shared/Assets/.gitkeep b/packages/in_app_purchase/in_app_purchase_storekit/shared/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart index dfc715c86b4d..e6369161080f 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/fakes/fake_storekit_platform.dart @@ -14,7 +14,9 @@ import '../store_kit_wrappers/sk_test_stub_objects.dart'; class FakeStoreKitPlatform { FakeStoreKitPlatform() { - channel.setMockMethodCallHandler(onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, onMethodCall); } // pre-configured store information @@ -235,3 +237,9 @@ class FakeStoreKitPlatform { return (call.arguments as Map).cast(); } } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart index dfdff5117091..2890e7542bbe 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_addtion_test.dart @@ -15,8 +15,10 @@ void main() { final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform(); setUpAll(() { - SystemChannels.platform - .setMockMethodCallHandler(fakeStoreKitPlatform.onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform, fakeStoreKitPlatform.onMethodCall); }); group('present code redemption sheet', () { @@ -39,3 +41,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart index 51ff2c229483..fbb37974a208 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/in_app_purchase_storekit_platform_test.dart @@ -21,8 +21,10 @@ void main() { late InAppPurchaseStoreKitPlatform iapStoreKitPlatform; setUpAll(() { - SystemChannels.platform - .setMockMethodCallHandler(fakeStoreKitPlatform.onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform, fakeStoreKitPlatform.onMethodCall); }); setUp(() { @@ -571,3 +573,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart index ff059ffbb1fc..0cf01b0bbfd6 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -14,8 +14,10 @@ void main() { final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform(); setUpAll(() { - SystemChannels.platform - .setMockMethodCallHandler(fakeStoreKitPlatform.onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform, fakeStoreKitPlatform.onMethodCall); }); setUp(() {}); @@ -185,7 +187,9 @@ void main() { class FakeStoreKitPlatform { FakeStoreKitPlatform() { - channel.setMockMethodCallHandler(onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, onMethodCall); } // get product request List startProductRequestParam = []; @@ -234,7 +238,7 @@ class FakeStoreKitPlatform { // receipt manager case '-[InAppPurchasePlugin retrieveReceiptData:result:]': if (getReceiptFailTest) { - throw 'some arbitrary error'; + throw Exception('some arbitrary error'); } return Future.value('receipt data'); // payment queue @@ -303,3 +307,9 @@ class TestPaymentTransactionObserver extends SKTransactionObserverWrapper { return true; } } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart index df76254aabf7..3d55fe27d7b0 100644 --- a/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart +++ b/packages/in_app_purchase/in_app_purchase_storekit/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart @@ -13,8 +13,10 @@ void main() { final FakeStoreKitPlatform fakeStoreKitPlatform = FakeStoreKitPlatform(); setUpAll(() { - SystemChannels.platform - .setMockMethodCallHandler(fakeStoreKitPlatform.onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform, fakeStoreKitPlatform.onMethodCall); }); test( @@ -148,7 +150,9 @@ class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper { class FakeStoreKitPlatform { FakeStoreKitPlatform() { - channel.setMockMethodCallHandler(onMethodCall); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, onMethodCall); } // indicate if the payment queue delegate is registered @@ -166,3 +170,9 @@ class FakeStoreKitPlatform { return Future.error('method not mocked'); } } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/ios_platform_images/CHANGELOG.md b/packages/ios_platform_images/CHANGELOG.md index 610c362a00db..72f1cf6d7d39 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.2.2 + +* Updates minimum version to iOS 11. + +## 0.2.1+1 + +* Add lint ignore comments + ## 0.2.1 * Updates minimum Flutter version to 3.3.0. diff --git a/packages/ios_platform_images/README.md b/packages/ios_platform_images/README.md index 08dfc3e40b31..9265b108595e 100644 --- a/packages/ios_platform_images/README.md +++ b/packages/ios_platform_images/README.md @@ -8,9 +8,9 @@ Flutter images. When loading images from Image.xcassets the device specific variant is chosen ([iOS documentation](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/image-size-and-resolution/)). -| | iOS | -|-------------|------| -| **Support** | 9.0+ | +| | iOS | +|-------------|-------| +| **Support** | 11.0+ | ## Usage diff --git a/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist b/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist index f2872cf474ee..4f8d4d2456f3 100644 --- a/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/ios_platform_images/example/ios/Podfile b/packages/ios_platform_images/example/ios/Podfile index 397864535f5d..fdcc671eb341 100644 --- a/packages/ios_platform_images/example/ios/Podfile +++ b/packages/ios_platform_images/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' 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 02e41bc13711..d6b4ef94bcef 100644 --- a/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -219,7 +219,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -278,10 +278,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -332,6 +334,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -457,7 +460,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -539,7 +542,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -588,7 +591,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 6de5fabfee04..7ae2cb4d4e54 100644 --- a/packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/ios_platform_images/example/pubspec.yaml b/packages/ios_platform_images/example/pubspec.yaml index 6045b3f67cfc..49b09bd8b637 100644 --- a/packages/ios_platform_images/example/pubspec.yaml +++ b/packages/ios_platform_images/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: cupertino_icons: ^1.0.2 diff --git a/packages/ios_platform_images/ios/ios_platform_images.podspec b/packages/ios_platform_images/ios/ios_platform_images.podspec index 3549277e9d86..02e5da149cd8 100644 --- a/packages/ios_platform_images/ios/ios_platform_images.podspec +++ b/packages/ios_platform_images/ios/ios_platform_images.podspec @@ -17,7 +17,7 @@ Downloaded by pub (not CocoaPods). s.documentation_url = 'https://pub.dev/packages/ios_platform_images' s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '9.0' + s.platform = :ios, '11.0' # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } diff --git a/packages/ios_platform_images/lib/ios_platform_images.dart b/packages/ios_platform_images/lib/ios_platform_images.dart index aeb875ad2463..b372d362f6f7 100644 --- a/packages/ios_platform_images/lib/ios_platform_images.dart +++ b/packages/ios_platform_images/lib/ios_platform_images.dart @@ -63,7 +63,9 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { @override ImageStreamCompleter loadBuffer( - _FutureMemoryImage key, DecoderBufferCallback decode) { + _FutureMemoryImage key, + DecoderBufferCallback decode, // ignore: deprecated_member_use + ) { return _FutureImageStreamCompleter( codec: _loadAsync(key, decode), futureScale: _futureScale, @@ -72,7 +74,7 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { Future _loadAsync( _FutureMemoryImage key, - DecoderBufferCallback decode, + DecoderBufferCallback decode, // ignore: deprecated_member_use ) { assert(key == this); return _futureBytes.then(ui.ImmutableBuffer.fromUint8List).then(decode); diff --git a/packages/ios_platform_images/pubspec.yaml b/packages/ios_platform_images/pubspec.yaml index 17fb8850ac1d..4193e3e339bf 100644 --- a/packages/ios_platform_images/pubspec.yaml +++ b/packages/ios_platform_images/pubspec.yaml @@ -2,10 +2,10 @@ name: ios_platform_images description: A plugin to share images between Flutter and iOS in add-to-app setups. repository: https://github.com/flutter/plugins/tree/main/packages/ios_platform_images issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+ios_platform_images%22 -version: 0.2.1 +version: 0.2.2 environment: - sdk: ">=2.14.0 <3.0.0" + sdk: '>=2.18.0 <3.0.0' flutter: ">=3.3.0" flutter: 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 76b012002dfa..f42b78646038 100644 --- a/packages/ios_platform_images/test/ios_platform_images_test.dart +++ b/packages/ios_platform_images/test/ios_platform_images_test.dart @@ -13,16 +13,26 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return '42'; }); }); tearDown(() { - channel.setMockMethodCallHandler(null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); }); test('resolveURL', () async { expect(await IosPlatformImages.resolveURL('foobar'), '42'); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/local_auth/local_auth/CHANGELOG.md b/packages/local_auth/local_auth/CHANGELOG.md index 21fed07d895b..0028704b34b1 100644 --- a/packages/local_auth/local_auth/CHANGELOG.md +++ b/packages/local_auth/local_auth/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.1.4 + +* Updates minimum Flutter version to 3.0. +* Updates documentation for Android version 8 and below theme compatibility. + ## 2.1.3 * Updates minimum Flutter version to 2.10. diff --git a/packages/local_auth/local_auth/README.md b/packages/local_auth/local_auth/README.md index a68692ea940a..8abf583b9dd4 100644 --- a/packages/local_auth/local_auth/README.md +++ b/packages/local_auth/local_auth/README.md @@ -252,6 +252,42 @@ types (such as face scanning) and you want to support SDKs lower than Q, _do not_ call `getAvailableBiometrics`. Simply call `authenticate` with `biometricOnly: true`. This will return an error if there was no hardware available. +#### Android theme + +Your `LaunchTheme`'s parent must be a valid `Theme.AppCompat` theme to prevent +crashes on Android 8 and below. For example, use `Theme.AppCompat.DayNight` to +enable light/dark modes for the biometric dialog. To do that go to +`android/app/src/main/res/values/styles.xml` and look for the style with name +`LaunchTheme`. Then change the parent for that style as follows: + +```xml +... + + + ... + +... +``` + +If you don't have a `styles.xml` file for your Android project you can set up +the Android theme directly in `android/app/src/main/AndroidManifest.xml`: + +```xml +... + + + +... +``` + ## Sticky Auth You can set the `stickyAuth` option on the plugin to true so that plugin does not diff --git a/packages/local_auth/local_auth/example/android/app/build.gradle b/packages/local_auth/local_auth/example/android/app/build.gradle index 3c6eca7ce8a7..0146852feb44 100644 --- a/packages/local_auth/local_auth/example/android/app/build.gradle +++ b/packages/local_auth/local_auth/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 33 lintOptions { disable 'InvalidPackage' diff --git a/packages/local_auth/local_auth/example/android/build.gradle b/packages/local_auth/local_auth/example/android/build.gradle index c21bff8e0a2f..3593d9636555 100644 --- a/packages/local_auth/local_auth/example/android/build.gradle +++ b/packages/local_auth/local_auth/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.3.1' } } diff --git a/packages/local_auth/local_auth/example/android/gradle.properties b/packages/local_auth/local_auth/example/android/gradle.properties index 7fe61a74cee0..e5611e4c7fa0 100644 --- a/packages/local_auth/local_auth/example/android/gradle.properties +++ b/packages/local_auth/local_auth/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1024m +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties index 3f383641d7c3..f5c5c374a4b7 100644 --- a/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/packages/local_auth/local_auth/example/lib/main.dart b/packages/local_auth/local_auth/example/lib/main.dart index f2cded002084..146a5d92b29c 100644 --- a/packages/local_auth/local_auth/example/lib/main.dart +++ b/packages/local_auth/local_auth/example/lib/main.dart @@ -183,6 +183,8 @@ class _MyAppState extends State { if (_isAuthenticating) ElevatedButton( onPressed: _cancelAuthentication, + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors child: Row( mainAxisSize: MainAxisSize.min, children: const [ @@ -196,6 +198,8 @@ class _MyAppState extends State { children: [ ElevatedButton( onPressed: _authenticate, + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors child: Row( mainAxisSize: MainAxisSize.min, children: const [ diff --git a/packages/local_auth/local_auth/example/pubspec.yaml b/packages/local_auth/local_auth/example/pubspec.yaml index f7dc2fc5b9e7..e02065b6d16f 100644 --- a/packages/local_auth/local_auth/example/pubspec.yaml +++ b/packages/local_auth/local_auth/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth/pubspec.yaml b/packages/local_auth/local_auth/pubspec.yaml index 722c993edb50..c2d3a007d10b 100644 --- a/packages/local_auth/local_auth/pubspec.yaml +++ b/packages/local_auth/local_auth/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for Android and iOS devices to allow local authentication via fingerprint, touch ID, face ID, passcode, pin, or pattern. repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 2.1.3 +version: 2.1.4 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/local_auth/local_auth_android/CHANGELOG.md b/packages/local_auth/local_auth_android/CHANGELOG.md index f1ef6a5c797c..92b671ca119f 100644 --- a/packages/local_auth/local_auth_android/CHANGELOG.md +++ b/packages/local_auth/local_auth_android/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.0.18 + +* Updates minimum Flutter version to 3.0. +* Updates androidx.core version to 1.9.0. +* Upgrades compile SDK version to 33. + ## 1.0.17 * Adds compatibility with `intl` 0.18.0. diff --git a/packages/local_auth/local_auth_android/android/build.gradle b/packages/local_auth/local_auth_android/android/build.gradle index 58684a67e418..8e116709d6cc 100644 --- a/packages/local_auth/local_auth_android/android/build.gradle +++ b/packages/local_auth/local_auth_android/android/build.gradle @@ -8,7 +8,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.2.1' + classpath 'com.android.tools.build:gradle:7.3.1' } } @@ -22,17 +22,14 @@ rootProject.allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 31 + compileSdkVersion 33 defaultConfig { minSdkVersion 16 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' baseline file("lint-baseline.xml") } @@ -51,11 +48,11 @@ android { } dependencies { - api "androidx.core:core:1.8.0" + api "androidx.core:core:1.9.0" api "androidx.biometric:biometric:1.1.0" api "androidx.fragment:fragment:1.5.5" testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'org.robolectric:robolectric:4.5' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' diff --git a/packages/local_auth/local_auth_android/example/android/app/build.gradle b/packages/local_auth/local_auth_android/example/android/app/build.gradle index 3c6eca7ce8a7..0146852feb44 100644 --- a/packages/local_auth/local_auth_android/example/android/app/build.gradle +++ b/packages/local_auth/local_auth_android/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 33 lintOptions { disable 'InvalidPackage' diff --git a/packages/local_auth/local_auth_android/example/android/build.gradle b/packages/local_auth/local_auth_android/example/android/build.gradle index c21bff8e0a2f..3593d9636555 100644 --- a/packages/local_auth/local_auth_android/example/android/build.gradle +++ b/packages/local_auth/local_auth_android/example/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.1' + classpath 'com.android.tools.build:gradle:7.3.1' } } diff --git a/packages/local_auth/local_auth_android/example/android/gradle.properties b/packages/local_auth/local_auth_android/example/android/gradle.properties index 7fe61a74cee0..e5611e4c7fa0 100644 --- a/packages/local_auth/local_auth_android/example/android/gradle.properties +++ b/packages/local_auth/local_auth_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1024m +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties index 3f383641d7c3..f5c5c374a4b7 100644 --- a/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/local_auth_android/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/packages/local_auth/local_auth_android/example/lib/main.dart b/packages/local_auth/local_auth_android/example/lib/main.dart index f7d908c81973..f245af973981 100644 --- a/packages/local_auth/local_auth_android/example/lib/main.dart +++ b/packages/local_auth/local_auth_android/example/lib/main.dart @@ -188,6 +188,8 @@ class _MyAppState extends State { if (_isAuthenticating) ElevatedButton( onPressed: _cancelAuthentication, + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors child: Row( mainAxisSize: MainAxisSize.min, children: const [ @@ -201,6 +203,8 @@ class _MyAppState extends State { children: [ ElevatedButton( onPressed: _authenticate, + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors child: Row( mainAxisSize: MainAxisSize.min, children: const [ diff --git a/packages/local_auth/local_auth_android/example/pubspec.yaml b/packages/local_auth/local_auth_android/example/pubspec.yaml index c95b89ad0c2a..fddd6b50f815 100644 --- a/packages/local_auth/local_auth_android/example/pubspec.yaml +++ b/packages/local_auth/local_auth_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_android/pubspec.yaml b/packages/local_auth/local_auth_android/pubspec.yaml index 3ad8a4b20a82..bc81476565cb 100644 --- a/packages/local_auth/local_auth_android/pubspec.yaml +++ b/packages/local_auth/local_auth_android/pubspec.yaml @@ -2,11 +2,11 @@ name: local_auth_android description: Android implementation of the local_auth plugin. repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.17 +version: 1.0.18 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/local_auth/local_auth_android/test/local_auth_test.dart b/packages/local_auth/local_auth_android/test/local_auth_test.dart index 86e5713f4bd6..136613d48245 100644 --- a/packages/local_auth/local_auth_android/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_android/test/local_auth_test.dart @@ -18,7 +18,9 @@ void main() { late LocalAuthAndroid localAuthentication; setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) { log.add(methodCall); switch (methodCall.method) { case 'getEnrolledBiometrics': @@ -174,3 +176,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/local_auth/local_auth_ios/CHANGELOG.md b/packages/local_auth/local_auth_ios/CHANGELOG.md index fd4c918262bb..eca9612fa69e 100644 --- a/packages/local_auth/local_auth_ios/CHANGELOG.md +++ b/packages/local_auth/local_auth_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 1.0.12 * Adds compatibility with `intl` 0.18.0. diff --git a/packages/local_auth/local_auth_ios/example/lib/main.dart b/packages/local_auth/local_auth_ios/example/lib/main.dart index 3aa8d6625232..63b317e54c7b 100644 --- a/packages/local_auth/local_auth_ios/example/lib/main.dart +++ b/packages/local_auth/local_auth_ios/example/lib/main.dart @@ -187,6 +187,8 @@ class _MyAppState extends State { if (_isAuthenticating) ElevatedButton( onPressed: _cancelAuthentication, + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors child: Row( mainAxisSize: MainAxisSize.min, children: const [ @@ -200,6 +202,8 @@ class _MyAppState extends State { children: [ ElevatedButton( onPressed: _authenticate, + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors child: Row( mainAxisSize: MainAxisSize.min, children: const [ diff --git a/packages/local_auth/local_auth_ios/example/pubspec.yaml b/packages/local_auth/local_auth_ios/example/pubspec.yaml index 720d5a732bd5..21b17fae7288 100644 --- a/packages/local_auth/local_auth_ios/example/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml index c9daa48c1fae..ef2fa7fcdac7 100644 --- a/packages/local_auth/local_auth_ios/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.0.12 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/local_auth/local_auth_ios/test/local_auth_test.dart b/packages/local_auth/local_auth_ios/test/local_auth_test.dart index 0ad89e52f5ce..0d7f56d5da90 100644 --- a/packages/local_auth/local_auth_ios/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_ios/test/local_auth_test.dart @@ -18,7 +18,9 @@ void main() { late LocalAuthIOS localAuthentication; setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) { log.add(methodCall); switch (methodCall.method) { case 'getEnrolledBiometrics': @@ -181,3 +183,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md index 7b9518b57589..be2be0ced788 100644 --- a/packages/local_auth/local_auth_platform_interface/CHANGELOG.md +++ b/packages/local_auth/local_auth_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 1.0.6 * Removes unused `intl` dependency. diff --git a/packages/local_auth/local_auth_platform_interface/pubspec.yaml b/packages/local_auth/local_auth_platform_interface/pubspec.yaml index 70b9d0e5f0b6..bc54978fd3df 100644 --- a/packages/local_auth/local_auth_platform_interface/pubspec.yaml +++ b/packages/local_auth/local_auth_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 1.0.6 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart index 824597ab2953..c513b4473574 100644 --- a/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart +++ b/packages/local_auth/local_auth_platform_interface/test/default_method_channel_platform_test.dart @@ -29,7 +29,9 @@ void main() { }); test('getAvailableBiometrics', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) { log.add(methodCall); return Future.value([]); }); @@ -49,7 +51,9 @@ void main() { // existing unendorsed implementations, used 'undefined' as a special // return value from `getAvailableBiometrics` to indicate that nothing was // enrolled, but that the hardware does support biometrics. - channel.setMockMethodCallHandler((MethodCall methodCall) { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) { log.add(methodCall); return Future.value(['undefined']); }); @@ -68,7 +72,9 @@ void main() { group('Boolean returning methods', () { setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) { log.add(methodCall); return Future.value(true); }); @@ -198,3 +204,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/local_auth/local_auth_windows/CHANGELOG.md b/packages/local_auth/local_auth_windows/CHANGELOG.md index b4f2061f2c27..90aa8b6b31db 100644 --- a/packages/local_auth/local_auth_windows/CHANGELOG.md +++ b/packages/local_auth/local_auth_windows/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 1.0.5 + +* Switches internal implementation to Pigeon. + ## 1.0.4 * Updates imports for `prefer_relative_imports`. diff --git a/packages/local_auth/local_auth_windows/example/lib/main.dart b/packages/local_auth/local_auth_windows/example/lib/main.dart index 546b635b8eca..3205cdb81bc8 100644 --- a/packages/local_auth/local_auth_windows/example/lib/main.dart +++ b/packages/local_auth/local_auth_windows/example/lib/main.dart @@ -108,44 +108,6 @@ class _MyAppState extends State { () => _authorized = authenticated ? 'Authorized' : 'Not Authorized'); } - Future _authenticateWithBiometrics() async { - bool authenticated = false; - try { - setState(() { - _isAuthenticating = true; - _authorized = 'Authenticating'; - }); - authenticated = await LocalAuthPlatform.instance.authenticate( - localizedReason: - 'Scan your fingerprint (or face or whatever) to authenticate', - authMessages: [const WindowsAuthMessages()], - options: const AuthenticationOptions( - 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; - } - - final String message = authenticated ? 'Authorized' : 'Not Authorized'; - setState(() { - _authorized = message; - }); - } - Future _cancelAuthentication() async { await LocalAuthPlatform.instance.stopAuthentication(); setState(() => _isAuthenticating = false); @@ -188,6 +150,8 @@ class _MyAppState extends State { if (_isAuthenticating) ElevatedButton( onPressed: _cancelAuthentication, + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors child: Row( mainAxisSize: MainAxisSize.min, children: const [ @@ -201,6 +165,8 @@ class _MyAppState extends State { children: [ ElevatedButton( onPressed: _authenticate, + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors child: Row( mainAxisSize: MainAxisSize.min, children: const [ @@ -209,18 +175,6 @@ class _MyAppState extends State { ], ), ), - ElevatedButton( - onPressed: _authenticateWithBiometrics, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(_isAuthenticating - ? 'Cancel' - : 'Authenticate: biometrics only'), - const Icon(Icons.fingerprint), - ], - ), - ), ], ), ], diff --git a/packages/local_auth/local_auth_windows/example/pubspec.yaml b/packages/local_auth/local_auth_windows/example/pubspec.yaml index 4bb2671f6826..1a1387a0875d 100644 --- a/packages/local_auth/local_auth_windows/example/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart b/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart index b373782c2187..9f918aab0585 100644 --- a/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart +++ b/packages/local_auth/local_auth_windows/lib/local_auth_windows.dart @@ -2,20 +2,25 @@ // 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/foundation.dart'; import 'package:local_auth_platform_interface/local_auth_platform_interface.dart'; -import 'types/auth_messages_windows.dart'; + +import 'src/messages.g.dart'; export 'package:local_auth_platform_interface/types/auth_messages.dart'; export 'package:local_auth_platform_interface/types/auth_options.dart'; export 'package:local_auth_platform_interface/types/biometric_type.dart'; export 'package:local_auth_windows/types/auth_messages_windows.dart'; -const MethodChannel _channel = - MethodChannel('plugins.flutter.io/local_auth_windows'); - /// The implementation of [LocalAuthPlatform] for Windows. class LocalAuthWindows extends LocalAuthPlatform { + /// Creates a new plugin implementation instance. + LocalAuthWindows({ + @visibleForTesting LocalAuthApi? api, + }) : _api = api ?? LocalAuthApi(); + + final LocalAuthApi _api; + /// Registers this class as the default instance of [LocalAuthPlatform]. static void registerWith() { LocalAuthPlatform.instance = LocalAuthWindows(); @@ -28,55 +33,36 @@ class LocalAuthWindows extends LocalAuthPlatform { AuthenticationOptions options = const AuthenticationOptions(), }) async { assert(localizedReason.isNotEmpty); - final Map args = { - 'localizedReason': localizedReason, - 'useErrorDialogs': options.useErrorDialogs, - 'stickyAuth': options.stickyAuth, - 'sensitiveTransaction': options.sensitiveTransaction, - 'biometricOnly': options.biometricOnly, - }; - args.addAll(const WindowsAuthMessages().args); - for (final AuthMessages messages in authMessages) { - if (messages is WindowsAuthMessages) { - args.addAll(messages.args); - } + + if (options.biometricOnly) { + throw UnsupportedError( + "Windows doesn't support the biometricOnly parameter."); } - return (await _channel.invokeMethod('authenticate', args)) ?? false; + + return _api.authenticate(localizedReason); } @override Future deviceSupportsBiometrics() async { - return (await _channel.invokeMethod('deviceSupportsBiometrics')) ?? - false; + // Biometrics are supported on any supported device. + return isDeviceSupported(); } @override Future> getEnrolledBiometrics() async { - final List result = (await _channel.invokeListMethod( - 'getEnrolledBiometrics', - )) ?? - []; - final List biometrics = []; - for (final String value in result) { - switch (value) { - case 'weak': - biometrics.add(BiometricType.weak); - break; - case 'strong': - biometrics.add(BiometricType.strong); - break; - } + // Windows doesn't support querying specific biometric types. Since the + // OS considers this a strong authentication API, return weak+strong on + // any supported device. + if (await isDeviceSupported()) { + return [BiometricType.weak, BiometricType.strong]; } - return biometrics; + return []; } @override - Future isDeviceSupported() async => - (await _channel.invokeMethod('isDeviceSupported')) ?? false; + Future isDeviceSupported() async => _api.isDeviceSupported(); /// Always returns false as this method is not supported on Windows. @override - Future stopAuthentication() async { - return false; - } + Future stopAuthentication() async => false; } diff --git a/packages/local_auth/local_auth_windows/lib/src/messages.g.dart b/packages/local_auth/local_auth_windows/lib/src/messages.g.dart new file mode 100644 index 000000000000..312d1c0ba164 --- /dev/null +++ b/packages/local_auth/local_auth_windows/lib/src/messages.g.dart @@ -0,0 +1,81 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v5.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +class LocalAuthApi { + /// Constructor for [LocalAuthApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + LocalAuthApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + /// Returns true if this device supports authentication. + Future isDeviceSupported() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LocalAuthApi.isDeviceSupported', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + /// Attempts to authenticate the user with the provided [localizedReason] as + /// the user-facing explanation for the authorization request. + /// + /// Returns true if authorization succeeds, false if it is attempted but is + /// not successful, and an error if authorization could not be attempted. + Future authenticate(String arg_localizedReason) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.LocalAuthApi.authenticate', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_localizedReason]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } +} diff --git a/packages/local_auth/local_auth_windows/pigeons/copyright.txt b/packages/local_auth/local_auth_windows/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/local_auth/local_auth_windows/pigeons/copyright.txt @@ -0,0 +1,3 @@ +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. diff --git a/packages/local_auth/local_auth_windows/pigeons/messages.dart b/packages/local_auth/local_auth_windows/pigeons/messages.dart new file mode 100644 index 000000000000..683becdd61fb --- /dev/null +++ b/packages/local_auth/local_auth_windows/pigeons/messages.dart @@ -0,0 +1,27 @@ +// 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:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + cppOptions: CppOptions(namespace: 'local_auth_windows'), + cppHeaderOut: 'windows/messages.g.h', + cppSourceOut: 'windows/messages.g.cpp', + copyrightHeader: 'pigeons/copyright.txt', +)) +@HostApi() +abstract class LocalAuthApi { + /// Returns true if this device supports authentication. + @async + bool isDeviceSupported(); + + /// Attempts to authenticate the user with the provided [localizedReason] as + /// the user-facing explanation for the authorization request. + /// + /// Returns true if authorization succeeds, false if it is attempted but is + /// not successful, and an error if authorization could not be attempted. + @async + bool authenticate(String localizedReason); +} diff --git a/packages/local_auth/local_auth_windows/pubspec.yaml b/packages/local_auth/local_auth_windows/pubspec.yaml index 9a2effed92ee..9866eef50584 100644 --- a/packages/local_auth/local_auth_windows/pubspec.yaml +++ b/packages/local_auth/local_auth_windows/pubspec.yaml @@ -2,11 +2,11 @@ name: local_auth_windows description: Windows implementation of the local_auth plugin. repository: https://github.com/flutter/plugins/tree/main/packages/local_auth/local_auth_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.0.4 +version: 1.0.5 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -24,3 +24,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + pigeon: ^5.0.1 diff --git a/packages/local_auth/local_auth_windows/test/local_auth_test.dart b/packages/local_auth/local_auth_windows/test/local_auth_test.dart index b11c19e7b339..917e7b1784b6 100644 --- a/packages/local_auth/local_auth_windows/test/local_auth_test.dart +++ b/packages/local_auth/local_auth_windows/test/local_auth_test.dart @@ -2,78 +2,123 @@ // 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:local_auth_windows/local_auth_windows.dart'; +import 'package:local_auth_windows/src/messages.g.dart'; void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - group('authenticate', () { - const MethodChannel channel = MethodChannel( - 'plugins.flutter.io/local_auth_windows', - ); - - final List log = []; - late LocalAuthWindows localAuthentication; + late _FakeLocalAuthApi api; + late LocalAuthWindows plugin; setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) { - log.add(methodCall); - switch (methodCall.method) { - case 'getEnrolledBiometrics': - return Future>.value(['weak', 'strong']); - default: - return Future.value(true); - } - }); - localAuthentication = LocalAuthWindows(); - log.clear(); + api = _FakeLocalAuthApi(); + plugin = LocalAuthWindows(api: api); }); - test('authenticate with no arguments passes expected defaults', () async { - await localAuthentication.authenticate( + test('authenticate handles success', () async { + api.returnValue = true; + + final bool result = await plugin.authenticate( authMessages: [const WindowsAuthMessages()], localizedReason: 'My localized reason'); - expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'My localized reason', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - 'biometricOnly': false, - }..addAll(const WindowsAuthMessages().args)), - ], - ); + + expect(result, true); + expect(api.passedReason, 'My localized reason'); + }); + + test('authenticate handles failure', () async { + api.returnValue = false; + + final bool result = await plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason'); + + expect(result, false); + expect(api.passedReason, 'My localized reason'); }); - test('authenticate passes all options.', () async { - await localAuthentication.authenticate( - authMessages: [const WindowsAuthMessages()], - localizedReason: 'My localized reason', - options: const AuthenticationOptions( - useErrorDialogs: false, - stickyAuth: true, - sensitiveTransaction: false, - biometricOnly: true, - ), - ); + test('authenticate throws for biometricOnly', () async { expect( - log, - [ - isMethodCall('authenticate', - arguments: { - 'localizedReason': 'My localized reason', - 'useErrorDialogs': false, - 'stickyAuth': true, - 'sensitiveTransaction': false, - 'biometricOnly': true, - }..addAll(const WindowsAuthMessages().args)), - ], - ); + plugin.authenticate( + authMessages: [const WindowsAuthMessages()], + localizedReason: 'My localized reason', + options: const AuthenticationOptions(biometricOnly: true)), + throwsA(isUnsupportedError)); + }); + + test('isDeviceSupported handles supported', () async { + api.returnValue = true; + + final bool result = await plugin.isDeviceSupported(); + + expect(result, true); + }); + + test('isDeviceSupported handles unsupported', () async { + api.returnValue = false; + + final bool result = await plugin.isDeviceSupported(); + + expect(result, false); + }); + + test('deviceSupportsBiometrics handles supported', () async { + api.returnValue = true; + + final bool result = await plugin.deviceSupportsBiometrics(); + + expect(result, true); + }); + + test('deviceSupportsBiometrics handles unsupported', () async { + api.returnValue = false; + + final bool result = await plugin.deviceSupportsBiometrics(); + + expect(result, false); + }); + + test('getEnrolledBiometrics returns expected values when supported', + () async { + api.returnValue = true; + + final List result = await plugin.getEnrolledBiometrics(); + + expect(result, [BiometricType.weak, BiometricType.strong]); + }); + + test('getEnrolledBiometrics returns nothing when unsupported', () async { + api.returnValue = false; + + final List result = await plugin.getEnrolledBiometrics(); + + expect(result, isEmpty); + }); + + test('stopAuthentication returns false', () async { + final bool result = await plugin.stopAuthentication(); + + expect(result, false); }); }); } + +class _FakeLocalAuthApi implements LocalAuthApi { + /// The return value for [isDeviceSupported] and [authenticate]. + bool returnValue = false; + + /// The argument that was passed to [authenticate]. + String? passedReason; + + @override + Future authenticate(String localizedReason) async { + passedReason = localizedReason; + return returnValue; + } + + @override + Future isDeviceSupported() async { + return returnValue; + } +} diff --git a/packages/local_auth/local_auth_windows/windows/CMakeLists.txt b/packages/local_auth/local_auth_windows/windows/CMakeLists.txt index bcf59bb827c7..9784aa5badd9 100644 --- a/packages/local_auth/local_auth_windows/windows/CMakeLists.txt +++ b/packages/local_auth/local_auth_windows/windows/CMakeLists.txt @@ -49,12 +49,14 @@ include_directories(BEFORE SYSTEM ${CMAKE_BINARY_DIR}/include) list(APPEND PLUGIN_SOURCES "local_auth_plugin.cpp" + "local_auth.h" + "messages.g.cpp" + "messages.g.h" ) add_library(${PLUGIN_NAME} SHARED "include/local_auth_windows/local_auth_plugin.h" "local_auth_windows.cpp" - "local_auth.h" ${PLUGIN_SOURCES} ) apply_standard_settings(${PLUGIN_NAME}) diff --git a/packages/local_auth/local_auth_windows/windows/local_auth.h b/packages/local_auth/local_auth_windows/windows/local_auth.h index 94b91f88345a..9cdc6efbcd15 100644 --- a/packages/local_auth/local_auth_windows/windows/local_auth.h +++ b/packages/local_auth/local_auth_windows/windows/local_auth.h @@ -10,8 +10,6 @@ #include #include -#include "include/local_auth_windows/local_auth_plugin.h" - // Include prior to C++/WinRT Headers #include #include @@ -23,6 +21,8 @@ #include #include +#include "messages.g.h" + namespace local_auth_windows { // Abstract class that is used to determine whether a user @@ -50,7 +50,7 @@ class UserConsentVerifier { UserConsentVerifier& operator=(const UserConsentVerifier&) = delete; }; -class LocalAuthPlugin : public flutter::Plugin { +class LocalAuthPlugin : public flutter::Plugin, public LocalAuthApi { public: static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar); @@ -62,28 +62,25 @@ class LocalAuthPlugin : public flutter::Plugin { // Exists for unit testing with mock implementations. LocalAuthPlugin(std::unique_ptr user_consent_verifier); - // Handles method calls from Dart on this plugin's channel. - void HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result); - virtual ~LocalAuthPlugin(); + // LocalAuthApi: + void IsDeviceSupported( + std::function reply)> result) override; + void Authenticate(const std::string& localized_reason, + std::function reply)> result) override; + private: std::unique_ptr user_consent_verifier_; // Starts authentication process. - winrt::fire_and_forget Authenticate( - const flutter::MethodCall& method_call, - std::unique_ptr> result); - - // Returns enrolled biometric types available on device. - winrt::fire_and_forget GetEnrolledBiometrics( - std::unique_ptr> result); + winrt::fire_and_forget AuthenticateCoroutine( + const std::string& localized_reason, + std::function reply)> result); // Returns whether the system supports Windows Hello. - winrt::fire_and_forget IsDeviceSupported( - std::unique_ptr> result); + winrt::fire_and_forget IsDeviceSupportedCoroutine( + std::function reply)> result); }; -} // namespace local_auth_windows \ No newline at end of file +} // namespace local_auth_windows diff --git a/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp b/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp index 7a25abb53010..80fab37ee50d 100644 --- a/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp +++ b/packages/local_auth/local_auth_windows/windows/local_auth_plugin.cpp @@ -4,24 +4,10 @@ #include #include "local_auth.h" +#include "messages.g.h" namespace { -template -// Helper method for getting an argument from an EncodableValue. -T GetArgument(const std::string arg, const flutter::EncodableValue* args, - T fallback) { - T result{fallback}; - const auto* arguments = std::get_if(args); - if (arguments) { - auto result_it = arguments->find(flutter::EncodableValue(arg)); - if (result_it != arguments->end()) { - result = std::get(result_it->second); - } - } - return result; -} - // Returns the window's HWND for a given FlutterView. HWND GetRootWindow(flutter::FlutterView* view) { return ::GetAncestor(view->GetNativeWindow(), GA_ROOT); @@ -110,19 +96,9 @@ class UserConsentVerifierImpl : public UserConsentVerifier { // static void LocalAuthPlugin::RegisterWithRegistrar( flutter::PluginRegistrarWindows* registrar) { - auto channel = - std::make_unique>( - registrar->messenger(), "plugins.flutter.io/local_auth_windows", - &flutter::StandardMethodCodec::GetInstance()); - auto plugin = std::make_unique( [registrar]() { return GetRootWindow(registrar->GetView()); }); - - channel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto& call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - + LocalAuthApi::SetUp(registrar->messenger(), plugin.get()); registrar->AddPlugin(std::move(plugin)); } @@ -137,36 +113,22 @@ LocalAuthPlugin::LocalAuthPlugin( LocalAuthPlugin::~LocalAuthPlugin() {} -void LocalAuthPlugin::HandleMethodCall( - const flutter::MethodCall& method_call, - std::unique_ptr> result) { - if (method_call.method_name().compare("authenticate") == 0) { - Authenticate(method_call, std::move(result)); - } else if (method_call.method_name().compare("getEnrolledBiometrics") == 0) { - GetEnrolledBiometrics(std::move(result)); - } else if (method_call.method_name().compare("isDeviceSupported") == 0 || - method_call.method_name().compare("deviceSupportsBiometrics") == - 0) { - IsDeviceSupported(std::move(result)); - } else { - result->NotImplemented(); - } +void LocalAuthPlugin::IsDeviceSupported( + std::function reply)> result) { + IsDeviceSupportedCoroutine(std::move(result)); +} + +void LocalAuthPlugin::Authenticate( + const std::string& localized_reason, + std::function reply)> result) { + AuthenticateCoroutine(localized_reason, std::move(result)); } // Starts authentication process. -winrt::fire_and_forget LocalAuthPlugin::Authenticate( - const flutter::MethodCall& method_call, - std::unique_ptr> result) { - std::wstring reason = Utf16FromUtf8(GetArgument( - "localizedReason", method_call.arguments(), std::string())); - - bool biometric_only = - GetArgument("biometricOnly", method_call.arguments(), false); - if (biometric_only) { - result->Error("biometricOnlyNotSupported", - "Windows doesn't support the biometricOnly parameter."); - co_return; - } +winrt::fire_and_forget LocalAuthPlugin::AuthenticateCoroutine( + const std::string& localized_reason, + std::function reply)> result) { + std::wstring reason = Utf16FromUtf8(localized_reason); winrt::Windows::Security::Credentials::UI::UserConsentVerifierAvailability ucv_availability = @@ -175,17 +137,19 @@ winrt::fire_and_forget LocalAuthPlugin::Authenticate( if (ucv_availability == winrt::Windows::Security::Credentials::UI:: UserConsentVerifierAvailability::DeviceNotPresent) { - result->Error("NoHardware", "No biometric hardware found"); + result(FlutterError("NoHardware", "No biometric hardware found")); co_return; } else if (ucv_availability == winrt::Windows::Security::Credentials::UI:: UserConsentVerifierAvailability::NotConfiguredForUser) { - result->Error("NotEnrolled", "No biometrics enrolled on this device."); + result( + FlutterError("NotEnrolled", "No biometrics enrolled on this device.")); co_return; } else if (ucv_availability != winrt::Windows::Security::Credentials::UI:: UserConsentVerifierAvailability::Available) { - result->Error("NotAvailable", "Required security features not enabled"); + result( + FlutterError("NotAvailable", "Required security features not enabled")); co_return; } @@ -195,42 +159,21 @@ winrt::fire_and_forget LocalAuthPlugin::Authenticate( co_await user_consent_verifier_->RequestVerificationForWindowAsync( reason); - result->Success(flutter::EncodableValue( - consent_result == winrt::Windows::Security::Credentials::UI:: - UserConsentVerificationResult::Verified)); + result(consent_result == winrt::Windows::Security::Credentials::UI:: + UserConsentVerificationResult::Verified); } catch (...) { - result->Success(flutter::EncodableValue(false)); - } -} - -// Returns biometric types available on device. -winrt::fire_and_forget LocalAuthPlugin::GetEnrolledBiometrics( - std::unique_ptr> result) { - try { - flutter::EncodableList biometrics; - winrt::Windows::Security::Credentials::UI::UserConsentVerifierAvailability - ucv_availability = - co_await user_consent_verifier_->CheckAvailabilityAsync(); - if (ucv_availability == winrt::Windows::Security::Credentials::UI:: - UserConsentVerifierAvailability::Available) { - biometrics.push_back(flutter::EncodableValue("weak")); - biometrics.push_back(flutter::EncodableValue("strong")); - } - result->Success(biometrics); - } catch (const std::exception& e) { - result->Error("no_biometrics_available", e.what()); + result(false); } } // Returns whether the device supports Windows Hello or not. -winrt::fire_and_forget LocalAuthPlugin::IsDeviceSupported( - std::unique_ptr> result) { +winrt::fire_and_forget LocalAuthPlugin::IsDeviceSupportedCoroutine( + std::function reply)> result) { winrt::Windows::Security::Credentials::UI::UserConsentVerifierAvailability ucv_availability = co_await user_consent_verifier_->CheckAvailabilityAsync(); - result->Success(flutter::EncodableValue( - ucv_availability == winrt::Windows::Security::Credentials::UI:: - UserConsentVerifierAvailability::Available)); + result(ucv_availability == winrt::Windows::Security::Credentials::UI:: + UserConsentVerifierAvailability::Available); } } // namespace local_auth_windows diff --git a/packages/local_auth/local_auth_windows/windows/messages.g.cpp b/packages/local_auth/local_auth_windows/windows/messages.g.cpp new file mode 100644 index 000000000000..e44b17c6a38d --- /dev/null +++ b/packages/local_auth/local_auth_windows/windows/messages.g.cpp @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v5.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#undef _HAS_EXCEPTIONS + +#include "messages.g.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace local_auth_windows { +/// The codec used by LocalAuthApi. +const flutter::StandardMessageCodec& LocalAuthApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance( + &flutter::StandardCodecSerializer::GetInstance()); +} + +// Sets up an instance of `LocalAuthApi` to handle messages through the +// `binary_messenger`. +void LocalAuthApi::SetUp(flutter::BinaryMessenger* binary_messenger, + LocalAuthApi* api) { + { + auto channel = + std::make_unique>( + binary_messenger, + "dev.flutter.pigeon.LocalAuthApi.isDeviceSupported", &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::EncodableValue& message, + const flutter::MessageReply& reply) { + try { + api->IsDeviceSupported([reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + flutter::EncodableList wrapped; + wrapped.push_back( + flutter::EncodableValue(std::move(output).TakeValue())); + reply(flutter::EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = + std::make_unique>( + binary_messenger, "dev.flutter.pigeon.LocalAuthApi.authenticate", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_localized_reason_arg = args.at(0); + if (encodable_localized_reason_arg.IsNull()) { + reply(WrapError("localized_reason_arg unexpectedly null.")); + return; + } + const auto& localized_reason_arg = + std::get(encodable_localized_reason_arg); + api->Authenticate( + localized_reason_arg, [reply](ErrorOr&& output) { + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + flutter::EncodableList wrapped; + wrapped.push_back( + flutter::EncodableValue(std::move(output).TakeValue())); + reply(flutter::EncodableValue(std::move(wrapped))); + }); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } +} + +flutter::EncodableValue LocalAuthApi::WrapError( + std::string_view error_message) { + return flutter::EncodableValue(flutter::EncodableList{ + flutter::EncodableValue(std::string(error_message)), + flutter::EncodableValue("Error"), flutter::EncodableValue()}); +} +flutter::EncodableValue LocalAuthApi::WrapError(const FlutterError& error) { + return flutter::EncodableValue(flutter::EncodableList{ + flutter::EncodableValue(error.message()), + flutter::EncodableValue(error.code()), error.details()}); +} + +} // namespace local_auth_windows diff --git a/packages/local_auth/local_auth_windows/windows/messages.g.h b/packages/local_auth/local_auth_windows/windows/messages.g.h new file mode 100644 index 000000000000..2ceff7732c90 --- /dev/null +++ b/packages/local_auth/local_auth_windows/windows/messages.g.h @@ -0,0 +1,93 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v5.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#ifndef PIGEON_LOCAL_AUTH_WINDOWS_H_ +#define PIGEON_LOCAL_AUTH_WINDOWS_H_ +#include +#include +#include +#include + +#include +#include +#include + +namespace local_auth_windows { + +// Generated class from Pigeon. + +class FlutterError { + public: + explicit FlutterError(const std::string& code) : code_(code) {} + explicit FlutterError(const std::string& code, const std::string& message) + : code_(code), message_(message) {} + explicit FlutterError(const std::string& code, const std::string& message, + const flutter::EncodableValue& details) + : code_(code), message_(message), details_(details) {} + + const std::string& code() const { return code_; } + const std::string& message() const { return message_; } + const flutter::EncodableValue& details() const { return details_; } + + private: + std::string code_; + std::string message_; + flutter::EncodableValue details_; +}; + +template +class ErrorOr { + public: + ErrorOr(const T& rhs) { new (&v_) T(rhs); } + ErrorOr(const T&& rhs) { v_ = std::move(rhs); } + ErrorOr(const FlutterError& rhs) { new (&v_) FlutterError(rhs); } + ErrorOr(const FlutterError&& rhs) { v_ = std::move(rhs); } + + bool has_error() const { return std::holds_alternative(v_); } + const T& value() const { return std::get(v_); }; + const FlutterError& error() const { return std::get(v_); }; + + private: + friend class LocalAuthApi; + ErrorOr() = default; + T TakeValue() && { return std::get(std::move(v_)); } + + std::variant v_; +}; + +// Generated interface from Pigeon that represents a handler of messages from +// Flutter. +class LocalAuthApi { + public: + LocalAuthApi(const LocalAuthApi&) = delete; + LocalAuthApi& operator=(const LocalAuthApi&) = delete; + virtual ~LocalAuthApi(){}; + // Returns true if this device supports authentication. + virtual void IsDeviceSupported( + std::function reply)> result) = 0; + // Attempts to authenticate the user with the provided [localizedReason] as + // the user-facing explanation for the authorization request. + // + // Returns true if authorization succeeds, false if it is attempted but is + // not successful, and an error if authorization could not be attempted. + virtual void Authenticate( + const std::string& localized_reason, + std::function reply)> result) = 0; + + // The codec used by LocalAuthApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `LocalAuthApi` to handle messages through the + // `binary_messenger`. + static void SetUp(flutter::BinaryMessenger* binary_messenger, + LocalAuthApi* api); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + + protected: + LocalAuthApi() = default; +}; +} // namespace local_auth_windows +#endif // PIGEON_LOCAL_AUTH_WINDOWS_H_ diff --git a/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp b/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp index 3828b05eef07..6b1b0ed79c3f 100644 --- a/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp +++ b/packages/local_auth/local_auth_windows/windows/test/local_auth_plugin_test.cpp @@ -4,10 +4,6 @@ #include "include/local_auth_windows/local_auth_plugin.h" -#include -#include -#include -#include #include #include #include @@ -32,9 +28,6 @@ using ::testing::Pointee; using ::testing::Return; TEST(LocalAuthPlugin, IsDeviceSupportedHandlerSuccessIfVerifierAvailable) { - std::unique_ptr result = - std::make_unique(); - std::unique_ptr mockConsentVerifier = std::make_unique(); @@ -48,48 +41,14 @@ TEST(LocalAuthPlugin, IsDeviceSupportedHandlerSuccessIfVerifierAvailable) { }); LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(false); + plugin.IsDeviceSupported([&result](ErrorOr reply) { result = reply; }); - EXPECT_CALL(*result, ErrorInternal).Times(0); - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); - - plugin.HandleMethodCall( - flutter::MethodCall("isDeviceSupported", - std::make_unique()), - std::move(result)); + EXPECT_FALSE(result.has_error()); + EXPECT_TRUE(result.value()); } TEST(LocalAuthPlugin, IsDeviceSupportedHandlerSuccessIfVerifierNotAvailable) { - std::unique_ptr result = - std::make_unique(); - - std::unique_ptr mockConsentVerifier = - std::make_unique(); - - EXPECT_CALL(*mockConsentVerifier, CheckAvailabilityAsync) - .Times(1) - .WillOnce([]() -> winrt::Windows::Foundation::IAsyncOperation< - winrt::Windows::Security::Credentials::UI:: - UserConsentVerifierAvailability> { - co_return winrt::Windows::Security::Credentials::UI:: - UserConsentVerifierAvailability::DeviceNotPresent; - }); - - LocalAuthPlugin plugin(std::move(mockConsentVerifier)); - - EXPECT_CALL(*result, ErrorInternal).Times(0); - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); - - plugin.HandleMethodCall( - flutter::MethodCall("isDeviceSupported", - std::make_unique()), - std::move(result)); -} - -TEST(LocalAuthPlugin, - GetEnrolledBiometricsHandlerReturnEmptyListIfVerifierNotAvailable) { - std::unique_ptr result = - std::make_unique(); - std::unique_ptr mockConsentVerifier = std::make_unique(); @@ -103,72 +62,14 @@ TEST(LocalAuthPlugin, }); LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(true); + plugin.IsDeviceSupported([&result](ErrorOr reply) { result = reply; }); - EXPECT_CALL(*result, ErrorInternal).Times(0); - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableList()))); - - plugin.HandleMethodCall( - flutter::MethodCall("getEnrolledBiometrics", - std::make_unique()), - std::move(result)); -} - -TEST(LocalAuthPlugin, - GetEnrolledBiometricsHandlerReturnNonEmptyListIfVerifierAvailable) { - std::unique_ptr result = - std::make_unique(); - - std::unique_ptr mockConsentVerifier = - std::make_unique(); - - EXPECT_CALL(*mockConsentVerifier, CheckAvailabilityAsync) - .Times(1) - .WillOnce([]() -> winrt::Windows::Foundation::IAsyncOperation< - winrt::Windows::Security::Credentials::UI:: - UserConsentVerifierAvailability> { - co_return winrt::Windows::Security::Credentials::UI:: - UserConsentVerifierAvailability::Available; - }); - - LocalAuthPlugin plugin(std::move(mockConsentVerifier)); - - EXPECT_CALL(*result, ErrorInternal).Times(0); - EXPECT_CALL(*result, - SuccessInternal(Pointee(EncodableList( - {EncodableValue("weak"), EncodableValue("strong")})))); - - plugin.HandleMethodCall( - flutter::MethodCall("getEnrolledBiometrics", - std::make_unique()), - std::move(result)); -} - -TEST(LocalAuthPlugin, AuthenticateHandlerDoesNotSupportBiometricOnly) { - std::unique_ptr result = - std::make_unique(); - - std::unique_ptr mockConsentVerifier = - std::make_unique(); - - LocalAuthPlugin plugin(std::move(mockConsentVerifier)); - - EXPECT_CALL(*result, ErrorInternal).Times(1); - EXPECT_CALL(*result, SuccessInternal).Times(0); - - std::unique_ptr args = - std::make_unique(EncodableMap({ - {"localizedReason", EncodableValue("My Reason")}, - {"biometricOnly", EncodableValue(true)}, - })); - - plugin.HandleMethodCall(flutter::MethodCall("authenticate", std::move(args)), - std::move(result)); + EXPECT_FALSE(result.has_error()); + EXPECT_FALSE(result.value()); } TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenAuthorized) { - std::unique_ptr result = - std::make_unique(); - std::unique_ptr mockConsentVerifier = std::make_unique(); @@ -193,24 +94,15 @@ TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenAuthorized) { }); LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(false); + plugin.Authenticate("My Reason", + [&result](ErrorOr reply) { result = reply; }); - EXPECT_CALL(*result, ErrorInternal).Times(0); - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); - - std::unique_ptr args = - std::make_unique(EncodableMap({ - {"localizedReason", EncodableValue("My Reason")}, - {"biometricOnly", EncodableValue(false)}, - })); - - plugin.HandleMethodCall(flutter::MethodCall("authenticate", std::move(args)), - std::move(result)); + EXPECT_FALSE(result.has_error()); + EXPECT_TRUE(result.value()); } TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenNotAuthorized) { - std::unique_ptr result = - std::make_unique(); - std::unique_ptr mockConsentVerifier = std::make_unique(); @@ -235,18 +127,12 @@ TEST(LocalAuthPlugin, AuthenticateHandlerWorksWhenNotAuthorized) { }); LocalAuthPlugin plugin(std::move(mockConsentVerifier)); + ErrorOr result(true); + plugin.Authenticate("My Reason", + [&result](ErrorOr reply) { result = reply; }); - EXPECT_CALL(*result, ErrorInternal).Times(0); - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); - - std::unique_ptr args = - std::make_unique(EncodableMap({ - {"localizedReason", EncodableValue("My Reason")}, - {"biometricOnly", EncodableValue(false)}, - })); - - plugin.HandleMethodCall(flutter::MethodCall("authenticate", std::move(args)), - std::move(result)); + EXPECT_FALSE(result.has_error()); + EXPECT_FALSE(result.value()); } } // namespace test diff --git a/packages/local_auth/local_auth_windows/windows/test/mocks.h b/packages/local_auth/local_auth_windows/windows/test/mocks.h index d82ae801b4b9..a31eb98aa7ef 100644 --- a/packages/local_auth/local_auth_windows/windows/test/mocks.h +++ b/packages/local_auth/local_auth_windows/windows/test/mocks.h @@ -5,10 +5,6 @@ #ifndef PACKAGES_LOCAL_AUTH_LOCAL_AUTH_WINDOWS_WINDOWS_TEST_MOCKS_H_ #define PACKAGES_LOCAL_AUTH_LOCAL_AUTH_WINDOWS_WINDOWS_TEST_MOCKS_H_ -#include -#include -#include -#include #include #include @@ -19,23 +15,8 @@ namespace test { namespace { -using flutter::EncodableMap; -using flutter::EncodableValue; using ::testing::_; -class MockMethodResult : public flutter::MethodResult<> { - public: - ~MockMethodResult() = default; - - MOCK_METHOD(void, SuccessInternal, (const EncodableValue* result), - (override)); - MOCK_METHOD(void, ErrorInternal, - (const std::string& error_code, const std::string& error_message, - const EncodableValue* details), - (override)); - MOCK_METHOD(void, NotImplementedInternal, (), (override)); -}; - class MockUserConsentVerifier : public UserConsentVerifier { public: explicit MockUserConsentVerifier(){}; diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index 436523551924..0f5e8e6d7225 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,5 +1,11 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 2.0.12 + +* Switches to the new `path_provider_foundation` implementation package + for iOS and macOS. * Updates code for `no_leading_underscores_for_local_identifiers` lint. * Updates minimum Flutter version to 2.10. * Fixes avoid_redundant_argument_values lint warnings and minor typos. diff --git a/packages/path_provider/path_provider/README.md b/packages/path_provider/path_provider/README.md index 3a52e3e72050..6a954d2ece61 100644 --- a/packages/path_provider/path_provider/README.md +++ b/packages/path_provider/path_provider/README.md @@ -36,7 +36,7 @@ Directories support by platform: | External Storage | ✔️ | ❌ | ❌ | ❌️ | ❌️ | | External Cache Directories | ✔️ | ❌ | ❌ | ❌️ | ❌️ | | External Storage Directories | ✔️ | ❌ | ❌ | ❌️ | ❌️ | -| Downloads | ❌ | ❌ | ✔️ | ✔️ | ✔️ | +| Downloads | ❌ | ✔️ | ✔️ | ✔️ | ✔️ | ## Testing diff --git a/packages/path_provider/path_provider/example/android/gradle.properties b/packages/path_provider/path_provider/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/path_provider/path_provider/example/android/gradle.properties +++ b/packages/path_provider/path_provider/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true 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 bf150f66f49b..f59a8faf31e0 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 @@ -87,19 +87,16 @@ void main() { } testWidgets('getDownloadsDirectory', (WidgetTester tester) async { - if (Platform.isIOS || Platform.isAndroid) { + if (Platform.isAndroid) { final Future result = getDownloadsDirectory(); expect(result, throwsA(isInstanceOf())); } else { final Directory? result = await getDownloadsDirectory(); - if (Platform.isMacOS) { - // On recent versions of macOS, actually using the downloads directory - // requires a user prompt, so will fail on CI. Instead, just check that - // it returned a path with the expected directory name. - expect(result?.path, endsWith('Downloads')); - } else { - _verifySampleFile(result, 'downloads'); - } + // On recent versions of macOS, actually using the downloads directory + // requires a user prompt (so will fail on CI), and on some platforms the + // directory may not exist. Instead of verifying that it exists, just + // check that it returned a path. + expect(result?.path, isNotEmpty); } }); } diff --git a/packages/path_provider/path_provider/example/pubspec.yaml b/packages/path_provider/path_provider/example/pubspec.yaml index 5964a267f96d..ffb878bcf146 100644 --- a/packages/path_provider/path_provider/example/pubspec.yaml +++ b/packages/path_provider/path_provider/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider/lib/path_provider.dart b/packages/path_provider/path_provider/lib/path_provider.dart index e89d29dc0036..b58a7ff6cc7b 100644 --- a/packages/path_provider/path_provider/lib/path_provider.dart +++ b/packages/path_provider/path_provider/lib/path_provider.dart @@ -45,11 +45,11 @@ PathProviderPlatform get _platform => PathProviderPlatform.instance; /// (and cleaning up) files or directories within this directory. This /// directory is scoped to the calling application. /// -/// On iOS, this uses the `NSCachesDirectory` API. +/// Example implementations: +/// - `NSCachesDirectory` on iOS and macOS. +/// - `Context.getCacheDir` on Android. /// -/// On Android, this uses the `getCacheDir` API on the context. -/// -/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// Throws a [MissingPlatformDirectoryException] if the system is unable to /// provide the directory. Future getTemporaryDirectory() async { final String? path = await _platform.getTemporaryPath(); @@ -63,15 +63,16 @@ Future getTemporaryDirectory() async { /// Path to a directory where the application may place application support /// files. /// +/// If this directory does not exist, it is created automatically. +/// /// Use this for files you don’t want exposed to the user. Your app should not /// use this directory for user data files. /// -/// On iOS, this uses the `NSApplicationSupportDirectory` API. -/// If this directory does not exist, it is created automatically. -/// -/// On Android, this function uses the `getFilesDir` API on the context. +/// Example implementations: +/// - `NSApplicationSupportDirectory` on iOS and macOS. +/// - The Flutter engine's `PathUtils.getFilesDir` API on Android. /// -/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// Throws a [MissingPlatformDirectoryException] if the system is unable to /// provide the directory. Future getApplicationSupportDirectory() async { final String? path = await _platform.getApplicationSupportPath(); @@ -86,10 +87,14 @@ Future getApplicationSupportDirectory() async { /// Path to the directory where application can store files that are persistent, /// backed up, and not visible to the user, such as sqlite.db. /// -/// On Android, this function throws an [UnsupportedError] as no equivalent -/// path exists. +/// Example implementations: +/// - `NSApplicationSupportDirectory` on iOS and macOS. +/// +/// Throws an [UnsupportedError] if this is not supported on the current +/// platform. For example, this is unlikely to ever be supported on Android, +/// as no equivalent path exists. /// -/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// 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(); @@ -102,14 +107,14 @@ Future getLibraryDirectory() async { /// Path to a directory where the application may place data that is /// user-generated, or that cannot otherwise be recreated by your application. /// -/// On iOS, this uses the `NSDocumentDirectory` API. Consider using -/// [getApplicationSupportDirectory] instead if the data is not user-generated. +/// Consider using another path, such as [getApplicationSupportDirectory] or +/// [getExternalStorageDirectory], if the data is not user-generated. /// -/// On Android, this uses the `getDataDirectory` API on the context. Consider -/// using [getExternalStorageDirectory] instead if data is intended to be visible -/// to the user. +/// Example implementations: +/// - `NSDocumentDirectory` on iOS and macOS. +/// - The Flutter engine's `PathUtils.getDataDirectory` API on Android. /// -/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// Throws a [MissingPlatformDirectoryException] if the system is unable to /// provide the directory. Future getApplicationDocumentsDirectory() async { final String? path = await _platform.getApplicationDocumentsPath(); @@ -121,13 +126,13 @@ Future getApplicationDocumentsDirectory() async { } /// 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. /// -/// On iOS, this function throws an [UnsupportedError] as it is not possible -/// to access outside the app's sandbox. +/// Example implementation: +/// - `getExternalFilesDir(null)` on Android. /// -/// On Android this uses the `getExternalFilesDir(null)`. +/// Throws an [UnsupportedError] if this is not supported on the current +/// platform (for example, on iOS where it is not possible to access outside +/// the app's sandbox). Future getExternalStorageDirectory() async { final String? path = await _platform.getExternalStoragePath(); if (path == null) { @@ -136,19 +141,19 @@ Future getExternalStorageDirectory() async { return Directory(path); } -/// Paths to directories where application specific external cache data can be -/// stored. These paths typically reside on external storage like separate -/// partitions or SD cards. Phones may have multiple storage directories -/// available. +/// Paths to directories where application specific cache data can be stored +/// externally. /// -/// The current operating system should be determined before issuing this -/// function call, as this functionality is only available on Android. +/// These paths typically reside on external storage like separate partitions +/// or SD cards. Phones may have multiple storage directories available. /// -/// On iOS, this function throws an UnsupportedError as it is not possible -/// to access outside the app's sandbox. +/// Example implementation: +/// - Context.getExternalCacheDirs() on Android (or +/// Context.getExternalCacheDir() on API levels below 19). /// -/// On Android this returns Context.getExternalCacheDirs() or -/// Context.getExternalCacheDir() on API levels below 19. +/// Throws an [UnsupportedError] if this is not supported on the current +/// platform. This is unlikely to ever be supported on any platform other than +/// Android. Future?> getExternalCacheDirectories() async { final List? paths = await _platform.getExternalCachePaths(); if (paths == null) { @@ -158,18 +163,19 @@ Future?> getExternalCacheDirectories() async { return paths.map((String path) => Directory(path)).toList(); } -/// Paths to directories where application specific data can be stored. +/// Paths to directories where application specific data can be stored +/// externally. +/// /// These paths typically reside on external storage like separate partitions /// or SD cards. Phones may have multiple storage directories available. /// -/// The current operating system should be determined before issuing this -/// function call, as this functionality is only available on Android. +/// Example implementation: +/// - Context.getExternalFilesDirs(type) on Android (or +/// Context.getExternalFilesDir(type) on API levels below 19). /// -/// On iOS, this function throws an UnsupportedError as it is not possible -/// to access outside the app's sandbox. -/// -/// On Android this returns Context.getExternalFilesDirs(String type) or -/// Context.getExternalFilesDir(String type) on API levels below 19. +/// Throws an [UnsupportedError] if this is not supported on the current +/// platform. This is unlikely to ever be supported on any platform other than +/// Android. Future?> getExternalStorageDirectories({ /// Optional parameter. See [StorageDirectory] for more informations on /// how this type translates to Android storage directories. @@ -185,10 +191,12 @@ Future?> getExternalStorageDirectories({ } /// Path to the directory where downloaded files can be stored. -/// This is typically only relevant on desktop operating systems. /// -/// On Android and on iOS, this function throws an [UnsupportedError] as no equivalent -/// path exists. +/// The returned directory is not guaranteed to exist, so clients should verify +/// that it does before using it, and potentially create it if necessary. +/// +/// Throws an [UnsupportedError] if this is not supported on the current +/// platform. Future getDownloadsDirectory() async { final String? path = await _platform.getDownloadsPath(); if (path == null) { diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index 8b68e1264fe7..8c139ccbb87b 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.0.11 +version: 2.0.12 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -14,11 +14,11 @@ flutter: android: default_package: path_provider_android ios: - default_package: path_provider_ios - macos: - default_package: path_provider_macos + default_package: path_provider_foundation linux: default_package: path_provider_linux + macos: + default_package: path_provider_foundation windows: default_package: path_provider_windows @@ -26,9 +26,8 @@ dependencies: flutter: sdk: flutter path_provider_android: ^2.0.6 - path_provider_ios: ^2.0.6 + path_provider_foundation: ^2.1.0 path_provider_linux: ^2.0.1 - path_provider_macos: ^2.0.0 path_provider_platform_interface: ^2.0.0 path_provider_windows: ^2.0.2 diff --git a/packages/path_provider/path_provider_android/CHANGELOG.md b/packages/path_provider/path_provider_android/CHANGELOG.md index 4edf0f8d290f..acf99b7a5e25 100644 --- a/packages/path_provider/path_provider_android/CHANGELOG.md +++ b/packages/path_provider/path_provider_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.0.22 * Removes unused Guava dependency. diff --git a/packages/path_provider/path_provider_android/android/build.gradle b/packages/path_provider/path_provider_android/android/build.gradle index c525cd388f5a..926142e5eaf8 100644 --- a/packages/path_provider/path_provider_android/android/build.gradle +++ b/packages/path_provider/path_provider_android/android/build.gradle @@ -29,10 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/packages/path_provider/path_provider_android/example/android/gradle.properties b/packages/path_provider/path_provider_android/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/path_provider/path_provider_android/example/android/gradle.properties +++ b/packages/path_provider/path_provider_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/path_provider/path_provider_android/example/pubspec.yaml b/packages/path_provider/path_provider_android/example/pubspec.yaml index b460d6ba49ce..e53c44ffda68 100644 --- a/packages/path_provider/path_provider_android/example/pubspec.yaml +++ b/packages/path_provider/path_provider_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_android/pubspec.yaml b/packages/path_provider/path_provider_android/pubspec.yaml index 9a8df83f4922..dcdf938feee5 100644 --- a/packages/path_provider/path_provider_android/pubspec.yaml +++ b/packages/path_provider/path_provider_android/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.0.22 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/path_provider/path_provider_foundation/CHANGELOG.md b/packages/path_provider/path_provider_foundation/CHANGELOG.md index fbdd426ba46d..7adb04f4c984 100644 --- a/packages/path_provider/path_provider_foundation/CHANGELOG.md +++ b/packages/path_provider/path_provider_foundation/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum supported Flutter version to 3.0. + +## 2.1.1 + +* Fixes a regression in the path retured by `getApplicationSupportDirectory` on iOS. + ## 2.1.0 * Renames the package previously published as diff --git a/packages/path_provider/path_provider_foundation/darwin/Classes/PathProviderPlugin.swift b/packages/path_provider/path_provider_foundation/darwin/Classes/PathProviderPlugin.swift index 770bcb31c529..af043090f545 100644 --- a/packages/path_provider/path_provider_foundation/darwin/Classes/PathProviderPlugin.swift +++ b/packages/path_provider/path_provider_foundation/darwin/Classes/PathProviderPlugin.swift @@ -24,12 +24,19 @@ public class PathProviderPlugin: NSObject, FlutterPlugin, PathProviderApi { func getDirectoryPath(type: DirectoryType) -> String? { var path = getDirectory(ofType: fileManagerDirectoryForType(type)) + #if os(macOS) + // In a non-sandboxed app, this is a shared directory where applications are + // expected to use its bundle ID as a subdirectory. (For non-sandboxed apps, + // adding the extra path is harmless). + // This is not done for iOS, for compatibility with older versions of the + // plugin. if type == .applicationSupport { if let basePath = path { let basePathURL = URL.init(fileURLWithPath: basePath) path = basePathURL.appendingPathComponent(Bundle.main.bundleIdentifier!).path } } + #endif return path } } diff --git a/packages/path_provider/path_provider_foundation/darwin/Tests/RunnerTests.swift b/packages/path_provider/path_provider_foundation/darwin/RunnerTests/RunnerTests.swift similarity index 80% rename from packages/path_provider/path_provider_foundation/darwin/Tests/RunnerTests.swift rename to packages/path_provider/path_provider_foundation/darwin/RunnerTests/RunnerTests.swift index 1f3e790ca1eb..99a56f2bfebf 100644 --- a/packages/path_provider/path_provider_foundation/darwin/Tests/RunnerTests.swift +++ b/packages/path_provider/path_provider_foundation/darwin/RunnerTests/RunnerTests.swift @@ -39,8 +39,19 @@ class RunnerTests: XCTestCase { func testGetApplicationSupportDirectory() throws { let plugin = PathProviderPlugin() let path = plugin.getDirectoryPath(type: .applicationSupport) - // The application support directory path should be the system application support - // path with an added subdirectory based on the app name. +#if os(iOS) + // On iOS, the application support directory path should be just the system application + // support path. + XCTAssertEqual( + path, + NSSearchPathForDirectoriesInDomains( + FileManager.SearchPathDirectory.applicationSupportDirectory, + FileManager.SearchPathDomainMask.userDomainMask, + true + ).first) +#else + // On macOS, the application support directory path should be the system application + // support path with an added subdirectory based on the app name. XCTAssert( path!.hasPrefix( NSSearchPathForDirectoriesInDomains( @@ -49,6 +60,7 @@ class RunnerTests: XCTestCase { true ).first!)) XCTAssert(path!.hasSuffix("Example")) +#endif } func testGetLibraryDirectory() throws { diff --git a/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.pbxproj index 866a166bbe45..70cdc7657d6d 100644 --- a/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/path_provider/path_provider_foundation/example/ios/Runner.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3380327729784D96002D32AE /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3380327629784D96002D32AE /* RunnerTests.swift */; }; + 33258D7929818305006BAA98 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33258D7729818302006BAA98 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 569E86265D93B926F433B2DF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 479D5DD53D431F6BBABA2E43 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; @@ -45,8 +45,8 @@ 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 = ""; }; 1E28C831B7D8EA9408BFB69A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 33258D7729818302006BAA98 /* RunnerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RunnerTests.swift; path = ../../darwin/RunnerTests/RunnerTests.swift; sourceTree = ""; }; 3380327429784D96002D32AE /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3380327629784D96002D32AE /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RunnerTests.swift; path = ../../../darwin/Tests/RunnerTests.swift; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 479D5DD53D431F6BBABA2E43 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5DB8EF5A2759054360D79B8D /* 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 = ""; }; @@ -87,12 +87,12 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 3380327529784D96002D32AE /* RunnerTests */ = { + 33258D76298182CC006BAA98 /* RunnerTests */ = { isa = PBXGroup; children = ( - 3380327629784D96002D32AE /* RunnerTests.swift */, + 33258D7729818302006BAA98 /* RunnerTests.swift */, ); - path = RunnerTests; + name = RunnerTests; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { @@ -111,7 +111,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - 3380327529784D96002D32AE /* RunnerTests */, + 33258D76298182CC006BAA98 /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, E1C876D20454FC3A1ED7F7E5 /* Pods */, C72F144CE69E83C4574EB334 /* Frameworks */, @@ -365,7 +365,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3380327729784D96002D32AE /* RunnerTests.swift in Sources */, + 33258D7929818305006BAA98 /* RunnerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/project.pbxproj index 54b137e735f2..5abc18a86297 100644 --- a/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/path_provider/path_provider_foundation/example/macos/Runner.xcodeproj/project.pbxproj @@ -82,7 +82,7 @@ 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 = ""; }; 33EBD3A726728EA70013E557 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 33EBD3A926728EA70013E557 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../../../darwin/Tests/RunnerTests.swift; sourceTree = ""; }; + 33EBD3A926728EA70013E557 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ../../../darwin/RunnerTests/RunnerTests.swift; sourceTree = ""; }; 33EBD3AB26728EA70013E557 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; 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 = ""; }; diff --git a/packages/path_provider/path_provider_foundation/example/pubspec.yaml b/packages/path_provider/path_provider_foundation/example/pubspec.yaml index 69524ad55e74..fcf599564659 100644 --- a/packages/path_provider/path_provider_foundation/example/pubspec.yaml +++ b/packages/path_provider/path_provider_foundation/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_foundation/pubspec.yaml b/packages/path_provider/path_provider_foundation/pubspec.yaml index 75d22e132793..30dd655acc00 100644 --- a/packages/path_provider/path_provider_foundation/pubspec.yaml +++ b/packages/path_provider/path_provider_foundation/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider_foundation description: iOS and macOS implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_foundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.1.0 +version: 2.1.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index baf3283348de..fa37eec3013b 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,6 +1,7 @@ -## NEXT +## 2.1.8 -* Updates minimum Flutter version to 2.10. +* Adds compatibility with `xdg_directories` 1.0. +* Updates minimum Flutter version to 3.0. ## 2.1.7 diff --git a/packages/path_provider/path_provider_linux/example/pubspec.yaml b/packages/path_provider/path_provider_linux/example/pubspec.yaml index 8d8940ba2f05..a305575bb13b 100644 --- a/packages/path_provider/path_provider_linux/example/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: "none" environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index 41d587360b5e..ecb9ea67525e 100644 --- a/packages/path_provider/path_provider_linux/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -2,11 +2,11 @@ name: path_provider_linux description: Linux implementation of the path_provider plugin repository: https://github.com/flutter/plugins/tree/main/packages/path_provider/path_provider_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 -version: 2.1.7 +version: 2.1.8 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -21,7 +21,7 @@ dependencies: sdk: flutter path: ^1.8.0 path_provider_platform_interface: ^2.0.0 - xdg_directories: ^0.2.0 + xdg_directories: ">=0.2.0 <2.0.0" dev_dependencies: flutter_test: diff --git a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md index f12e1ec53ade..e3470dc36844 100644 --- a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md +++ b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.0.5 * Updates imports for `prefer_relative_imports`. diff --git a/packages/path_provider/path_provider_platform_interface/pubspec.yaml b/packages/path_provider/path_provider_platform_interface/pubspec.yaml index 6ce7ec662b33..3ce20f6f85db 100644 --- a/packages/path_provider/path_provider_platform_interface/pubspec.yaml +++ b/packages/path_provider/path_provider_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 2.0.5 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: 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 69c9b2b01f19..035e7becb9ff 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 @@ -25,8 +25,10 @@ void main() { setUp(() async { methodChannelPathProvider = MethodChannelPathProvider(); - methodChannelPathProvider.methodChannel - .setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(methodChannelPathProvider.methodChannel, + (MethodCall methodCall) async { log.add(methodCall); switch (methodCall.method) { case 'getTemporaryDirectory': @@ -204,3 +206,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md index 757f13dbb533..08920a9569e8 100644 --- a/packages/path_provider/path_provider_windows/CHANGELOG.md +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.1.3 * Updates minimum Flutter version to 2.10. diff --git a/packages/path_provider/path_provider_windows/example/pubspec.yaml b/packages/path_provider/path_provider_windows/example/pubspec.yaml index d70a4a84f504..306f20c354df 100644 --- a/packages/path_provider/path_provider_windows/example/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/plugin_platform_interface/CHANGELOG.md b/packages/plugin_platform_interface/CHANGELOG.md index 0b5a6b63a52f..93e45c814668 100644 --- a/packages/plugin_platform_interface/CHANGELOG.md +++ b/packages/plugin_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum supported Dart version. + ## 2.1.3 * Minor fixes for new analysis options. diff --git a/packages/plugin_platform_interface/pubspec.yaml b/packages/plugin_platform_interface/pubspec.yaml index 6a4bc488693b..25189d942f84 100644 --- a/packages/plugin_platform_interface/pubspec.yaml +++ b/packages/plugin_platform_interface/pubspec.yaml @@ -18,7 +18,7 @@ issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+ version: 2.1.3 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" dependencies: meta: ^1.3.0 diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md index 7d1881596255..0787c27014f1 100644 --- a/packages/quick_actions/quick_actions/CHANGELOG.md +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 1.0.1 * Updates implementaion package versions to current versions. diff --git a/packages/quick_actions/quick_actions/example/android/gradle.properties b/packages/quick_actions/quick_actions/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/quick_actions/quick_actions/example/android/gradle.properties +++ b/packages/quick_actions/quick_actions/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/quick_actions/quick_actions/example/pubspec.yaml b/packages/quick_actions/quick_actions/example/pubspec.yaml index 1a10a653db06..c629384ee5e2 100644 --- a/packages/quick_actions/quick_actions/example/pubspec.yaml +++ b/packages/quick_actions/quick_actions/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions/pubspec.yaml b/packages/quick_actions/quick_actions/pubspec.yaml index 08b486fe50e3..3f1bf57a70f0 100644 --- a/packages/quick_actions/quick_actions/pubspec.yaml +++ b/packages/quick_actions/quick_actions/pubspec.yaml @@ -7,7 +7,7 @@ version: 1.0.1 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/quick_actions/quick_actions_android/CHANGELOG.md b/packages/quick_actions/quick_actions_android/CHANGELOG.md index bc809a4dc477..6587627b2145 100644 --- a/packages/quick_actions/quick_actions_android/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 1.0.0 * Updates version to 1.0 to reflect current status. diff --git a/packages/quick_actions/quick_actions_android/android/build.gradle b/packages/quick_actions/quick_actions_android/android/build.gradle index e4cdec819ec9..4291fa020ef9 100644 --- a/packages/quick_actions/quick_actions_android/android/build.gradle +++ b/packages/quick_actions/quick_actions_android/android/build.gradle @@ -29,15 +29,12 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - // TODO(stuartmorgan): Enable when gradle is updated. - // disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } dependencies { testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:5.0.0' } compileOptions { diff --git a/packages/quick_actions/quick_actions_android/example/android/app/build.gradle b/packages/quick_actions/quick_actions_android/example/android/app/build.gradle index 666194bc11b0..c9cbddb9ffeb 100644 --- a/packages/quick_actions/quick_actions_android/example/android/app/build.gradle +++ b/packages/quick_actions/quick_actions_android/example/android/app/build.gradle @@ -62,6 +62,6 @@ dependencies { androidTestImplementation "androidx.test:runner:$androidXTestVersion" androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' androidTestImplementation 'androidx.test.ext:junit:1.0.0' - androidTestImplementation 'org.mockito:mockito-core:4.7.0' - androidTestImplementation 'org.mockito:mockito-android:4.7.0' + androidTestImplementation 'org.mockito:mockito-core:5.0.0' + androidTestImplementation 'org.mockito:mockito-android:5.0.0' } diff --git a/packages/quick_actions/quick_actions_android/example/android/gradle.properties b/packages/quick_actions/quick_actions_android/example/android/gradle.properties index 38c8d4544ff1..2f3603c9ff62 100644 --- a/packages/quick_actions/quick_actions_android/example/android/gradle.properties +++ b/packages/quick_actions/quick_actions_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.enableR8=true android.useAndroidX=true android.enableJetifier=true diff --git a/packages/quick_actions/quick_actions_android/example/pubspec.yaml b/packages/quick_actions/quick_actions_android/example/pubspec.yaml index c560d4dd5f1e..48a6fe9fd1a5 100644 --- a/packages/quick_actions/quick_actions_android/example/pubspec.yaml +++ b/packages/quick_actions/quick_actions_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions_android/pubspec.yaml b/packages/quick_actions/quick_actions_android/pubspec.yaml index e47a1fdc13e9..038c8631287f 100644 --- a/packages/quick_actions/quick_actions_android/pubspec.yaml +++ b/packages/quick_actions/quick_actions_android/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.0.0 environment: sdk: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart b/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart index 40cfe458615d..0a98f5d4e55b 100644 --- a/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart +++ b/packages/quick_actions/quick_actions_android/test/quick_actions_android_test.dart @@ -21,8 +21,10 @@ void main() { QuickActionsAndroid buildQuickActionsPlugin() { final QuickActionsAndroid quickActions = QuickActionsAndroid(); - quickActions.channel - .setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(quickActions.channel, + (MethodCall methodCall) async { log.add(methodCall); return ''; }); @@ -162,3 +164,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/quick_actions/quick_actions_ios/CHANGELOG.md b/packages/quick_actions/quick_actions_ios/CHANGELOG.md index bded35478899..e135fa4c9b69 100644 --- a/packages/quick_actions/quick_actions_ios/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 1.0.2 * Migrates remaining components to Swift and removes all Objective-C settings. diff --git a/packages/quick_actions/quick_actions_ios/example/pubspec.yaml b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml index ecac371720d6..af0697022ea3 100644 --- a/packages/quick_actions/quick_actions_ios/example/pubspec.yaml +++ b/packages/quick_actions/quick_actions_ios/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions_ios/pubspec.yaml b/packages/quick_actions/quick_actions_ios/pubspec.yaml index 6e7fb43dd7ed..2b7572368773 100644 --- a/packages/quick_actions/quick_actions_ios/pubspec.yaml +++ b/packages/quick_actions/quick_actions_ios/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.0.2 environment: sdk: ">=2.15.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/quick_actions/quick_actions_ios/test/quick_actions_ios_test.dart b/packages/quick_actions/quick_actions_ios/test/quick_actions_ios_test.dart index 36827a5c6d8c..d2b062fff223 100644 --- a/packages/quick_actions/quick_actions_ios/test/quick_actions_ios_test.dart +++ b/packages/quick_actions/quick_actions_ios/test/quick_actions_ios_test.dart @@ -21,8 +21,10 @@ void main() { QuickActionsIos buildQuickActionsPlugin() { final QuickActionsIos quickActions = QuickActionsIos(); - quickActions.channel - .setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(quickActions.channel, + (MethodCall methodCall) async { log.add(methodCall); return ''; }); @@ -162,3 +164,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md b/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md index 950864f96653..6bbfd5a35f67 100644 --- a/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md +++ b/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 1.0.3 * Updates imports for `prefer_relative_imports`. diff --git a/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml b/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml index 2990da603c14..cfde0a76f5b2 100644 --- a/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml +++ b/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 1.0.3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart b/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart index 240f11bd8037..c1a508fbfb92 100644 --- a/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart +++ b/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart @@ -18,8 +18,10 @@ void main() { final List log = []; setUp(() { - quickActions.channel - .setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(quickActions.channel, + (MethodCall methodCall) async { log.add(methodCall); return ''; }); @@ -148,3 +150,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 6fe9568afa79..ed44436dfe1e 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 2.0.17 + +* Updates code for stricter lint checks. + ## 2.0.16 * Switches to the new `shared_preferences_foundation` implementation package diff --git a/packages/shared_preferences/shared_preferences/example/android/gradle.properties b/packages/shared_preferences/shared_preferences/example/android/gradle.properties index 94adc3a3f97a..598d13fee446 100644 --- a/packages/shared_preferences/shared_preferences/example/android/gradle.properties +++ b/packages/shared_preferences/shared_preferences/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/shared_preferences/shared_preferences/example/lib/main.dart b/packages/shared_preferences/shared_preferences/example/lib/main.dart index a2e72b446925..f9690395f10d 100644 --- a/packages/shared_preferences/shared_preferences/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences/example/lib/main.dart @@ -66,9 +66,11 @@ class SharedPreferencesDemoState extends State { future: _counter, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.none: case ConnectionState.waiting: return const CircularProgressIndicator(); - default: + case ConnectionState.active: + case ConnectionState.done: if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else { diff --git a/packages/shared_preferences/shared_preferences/example/pubspec.yaml b/packages/shared_preferences/shared_preferences/example/pubspec.yaml index 6964656d16ef..944538da0d0c 100644 --- a/packages/shared_preferences/shared_preferences/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index cfa7f005e970..30ee569c3ad3 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.16 +version: 2.0.17 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md index d9d7bbd82f46..727f2b626d81 100644 --- a/packages/shared_preferences/shared_preferences_android/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 2.0.15 + +* Updates code for stricter lint checks. + ## 2.0.14 * Fixes typo in `SharedPreferencesAndroid` docs. diff --git a/packages/shared_preferences/shared_preferences_android/android/build.gradle b/packages/shared_preferences/shared_preferences_android/android/build.gradle index 480e815d06f1..29f946bf7f77 100644 --- a/packages/shared_preferences/shared_preferences_android/android/build.gradle +++ b/packages/shared_preferences/shared_preferences_android/android/build.gradle @@ -37,9 +37,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' baseline file("lint-baseline.xml") } dependencies { diff --git a/packages/shared_preferences/shared_preferences_android/example/android/gradle.properties b/packages/shared_preferences/shared_preferences_android/example/android/gradle.properties index 94adc3a3f97a..598d13fee446 100644 --- a/packages/shared_preferences/shared_preferences_android/example/android/gradle.properties +++ b/packages/shared_preferences/shared_preferences_android/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/shared_preferences/shared_preferences_android/example/lib/main.dart b/packages/shared_preferences/shared_preferences_android/example/lib/main.dart index bb513b09f6d5..cbcad6391beb 100644 --- a/packages/shared_preferences/shared_preferences_android/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_android/example/lib/main.dart @@ -70,9 +70,11 @@ class SharedPreferencesDemoState extends State { future: _counter, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.none: case ConnectionState.waiting: return const CircularProgressIndicator(); - default: + case ConnectionState.active: + case ConnectionState.done: if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else { diff --git a/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml index bd1272c71d80..c0bc6668e3dd 100644 --- a/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_android/pubspec.yaml b/packages/shared_preferences/shared_preferences_android/pubspec.yaml index 7692c114bfce..d968dcbce55b 100644 --- a/packages/shared_preferences/shared_preferences_android/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_android/pubspec.yaml @@ -2,11 +2,11 @@ name: shared_preferences_android description: Android implementation of the shared_preferences plugin repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.0.14 +version: 2.0.15 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart index fb6764893651..f1043daac1a4 100644 --- a/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart +++ b/packages/shared_preferences/shared_preferences_android/test/shared_preferences_android_test.dart @@ -38,7 +38,9 @@ void main() { .cast(); } - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); if (methodCall.method == 'getAll') { return testData.getAll(); @@ -124,3 +126,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md b/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md index 0a9baa072f53..b178143ca0b8 100644 --- a/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_foundation/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2.1.3 + +* Uses the new `sharedDarwinSource` flag when available. +* Updates minimum Flutter version to 3.0. + +## 2.1.2 + +* Updates code for stricter lint checks. + ## 2.1.1 * Adds Swift runtime search paths in podspec to avoid crash in Objective-C apps. diff --git a/packages/shared_preferences/shared_preferences_foundation/example/lib/main.dart b/packages/shared_preferences/shared_preferences_foundation/example/lib/main.dart index e6bbe5931471..a5aedd54ab6f 100644 --- a/packages/shared_preferences/shared_preferences_foundation/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_foundation/example/lib/main.dart @@ -72,9 +72,11 @@ class SharedPreferencesDemoState extends State { future: _counter, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.none: case ConnectionState.waiting: return const CircularProgressIndicator(); - default: + case ConnectionState.active: + case ConnectionState.done: if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else { diff --git a/packages/shared_preferences/shared_preferences_foundation/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_foundation/example/pubspec.yaml index 713f7b0c9ffe..ef67f234e7c5 100644 --- a/packages/shared_preferences/shared_preferences_foundation/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_foundation/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml b/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml index 8e65d1bda9b6..3deb07fc5960 100644 --- a/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_foundation/pubspec.yaml @@ -2,24 +2,24 @@ name: shared_preferences_foundation description: iOS and macOS implementation of the shared_preferences plugin. repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_foundation issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.1.1 +version: 2.1.3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: implements: shared_preferences platforms: - # TODO(stuartmorgan): Add sharedDarwinSource to these once - # https://github.com/flutter/flutter/pull/115337 lands. ios: pluginClass: SharedPreferencesPlugin dartPluginClass: SharedPreferencesFoundation + sharedDarwinSource: true macos: pluginClass: SharedPreferencesPlugin dartPluginClass: SharedPreferencesFoundation + sharedDarwinSource: true dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index 7c518fcf71cb..3c5a398546d1 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 2.1.3 + +* Updates code for stricter lint checks. + ## 2.1.2 * Updates code for stricter lint checks. 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 d51be33baeed..a904c824d4fe 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart @@ -66,9 +66,11 @@ class SharedPreferencesDemoState extends State { future: _counter, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.none: case ConnectionState.waiting: return const CircularProgressIndicator(); - default: + case ConnectionState.active: + case ConnectionState.done: if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else { diff --git a/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml index 9418c0581ed7..98ff24a84682 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index cd2ca8007e3c..21203a877586 100644 --- a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml @@ -2,11 +2,11 @@ name: shared_preferences_linux description: Linux implementation of the shared_preferences plugin repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.1.2 +version: 2.1.3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md index 3ef89c396222..38cdf083ccda 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md @@ -1,6 +1,6 @@ ## NEXT -* Updates minimum Flutter version to 2.10. +* Updates minimum Flutter version to 3.0. ## 2.1.0 diff --git a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml index b55eb1ccceb2..59d6409cff7a 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.1.0 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: 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 da333cf7f234..296592e70bb0 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 @@ -36,7 +36,9 @@ void main() { .cast(); } - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); if (methodCall.method == 'getAll') { return testData.getAll(); @@ -117,3 +119,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index 8c8411da6fff..6332663b4b47 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,6 +1,6 @@ ## NEXT -* Updates minimum Flutter version to 2.10. +* Updates minimum Flutter version to 3.0. ## 2.0.4 diff --git a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml index 050275489efa..52cfa1b436fb 100644 --- a/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml index b64f37d10da6..942fe12a39a1 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.0.4 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md index c486a4ce8f9b..b99e3dd6f6ec 100644 --- a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 2.1.3 + +* Updates code for stricter lint checks. + ## 2.1.2 * Updates code for stricter lint checks. 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 74d5e4c68772..e442c4b69ee5 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart @@ -66,9 +66,11 @@ class SharedPreferencesDemoState extends State { future: _counter, builder: (BuildContext context, AsyncSnapshot snapshot) { switch (snapshot.connectionState) { + case ConnectionState.none: case ConnectionState.waiting: return const CircularProgressIndicator(); - default: + case ConnectionState.active: + case ConnectionState.done: if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else { diff --git a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml index 43c2145b32ae..bb51f7fbef18 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml index 7f97759e6364..03fc31c6301e 100644 --- a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml @@ -2,11 +2,11 @@ name: shared_preferences_windows description: Windows implementation of shared_preferences repository: https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 -version: 2.1.2 +version: 2.1.3 environment: sdk: '>=2.12.0 <3.0.0' - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 4b365b8d858d..4079520d9120 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,12 @@ +## 6.1.9 + +* Updates minimum Flutter version to 3.0. +* Updates iOS minimum version in README. + +## 6.1.8 + +* Updates code for stricter lint checks. + ## 6.1.7 * Updates code for new analysis options. diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index e9e4dae476cc..b394e4ad6395 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -6,9 +6,9 @@ A Flutter plugin for launching a URL. -| | Android | iOS | Linux | macOS | Web | Windows | -|-------------|---------|------|-------|--------|-----|-------------| -| **Support** | SDK 16+ | 9.0+ | Any | 10.11+ | Any | Windows 10+ | +| | Android | iOS | Linux | macOS | Web | Windows | +|-------------|---------|-------|-------|--------|-----|-------------| +| **Support** | SDK 16+ | 11.0+ | Any | 10.11+ | Any | Windows 10+ | ## Usage @@ -38,7 +38,7 @@ void main() => runApp( Future _launchUrl() async { if (!await launchUrl(_url)) { - throw 'Could not launch $_url'; + throw Exception('Could not launch $_url'); } } ``` @@ -198,10 +198,10 @@ final String filePath = testFile.absolute.path; final Uri uri = Uri.file(filePath); if (!File(uri.toFilePath()).existsSync()) { - throw '$uri does not exist!'; + throw Exception('$uri does not exist!'); } if (!await launchUrl(uri)) { - throw 'Could not launch $uri'; + throw Exception('Could not launch $uri'); } ``` diff --git a/packages/url_launcher/url_launcher/example/android/gradle.properties b/packages/url_launcher/url_launcher/example/android/gradle.properties index a6738207fd15..e5611e4c7fa0 100644 --- a/packages/url_launcher/url_launcher/example/android/gradle.properties +++ b/packages/url_launcher/url_launcher/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist b/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9b41e7d87980 100644 --- a/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/url_launcher/url_launcher/example/ios/Podfile b/packages/url_launcher/url_launcher/example/ios/Podfile index f7d6a5e68c3a..d207307f86d7 100644 --- a/packages/url_launcher/url_launcher/example/ios/Podfile +++ b/packages/url_launcher/url_launcher/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj index 7855640c017e..0b8010748e09 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -169,7 +169,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -213,10 +213,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -227,6 +229,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -340,7 +343,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -390,7 +393,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c5f1a9de4a30..ad0ebfab1b88 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/url_launcher/url_launcher/example/lib/basic.dart b/packages/url_launcher/url_launcher/example/lib/basic.dart index 987ca2134318..422e2aae460c 100644 --- a/packages/url_launcher/url_launcher/example/lib/basic.dart +++ b/packages/url_launcher/url_launcher/example/lib/basic.dart @@ -28,7 +28,7 @@ void main() => runApp( Future _launchUrl() async { if (!await launchUrl(_url)) { - throw 'Could not launch $_url'; + throw Exception('Could not launch $_url'); } } // #enddocregion basic-example diff --git a/packages/url_launcher/url_launcher/example/lib/encoding.dart b/packages/url_launcher/url_launcher/example/lib/encoding.dart index 24c724466a77..575eb5f42387 100644 --- a/packages/url_launcher/url_launcher/example/lib/encoding.dart +++ b/packages/url_launcher/url_launcher/example/lib/encoding.dart @@ -22,8 +22,14 @@ String? encodeQueryParameters(Map params) { // #enddocregion encode-query-parameters void main() => runApp( + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors MaterialApp( + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors home: Material( + // TODO(goderbauer): Make this const when this package requires Flutter 3.8 or later. + // ignore: prefer_const_constructors child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: const [ diff --git a/packages/url_launcher/url_launcher/example/lib/files.dart b/packages/url_launcher/url_launcher/example/lib/files.dart index d48440670406..7f9d20669ee7 100644 --- a/packages/url_launcher/url_launcher/example/lib/files.dart +++ b/packages/url_launcher/url_launcher/example/lib/files.dart @@ -38,10 +38,10 @@ Future _openFile() async { final Uri uri = Uri.file(filePath); if (!File(uri.toFilePath()).existsSync()) { - throw '$uri does not exist!'; + throw Exception('$uri does not exist!'); } if (!await launchUrl(uri)) { - throw 'Could not launch $uri'; + throw Exception('Could not launch $uri'); } // #enddocregion file } diff --git a/packages/url_launcher/url_launcher/example/lib/main.dart b/packages/url_launcher/url_launcher/example/lib/main.dart index a870e18a53de..9b005cf98db0 100644 --- a/packages/url_launcher/url_launcher/example/lib/main.dart +++ b/packages/url_launcher/url_launcher/example/lib/main.dart @@ -58,7 +58,7 @@ class _MyHomePageState extends State { url, mode: LaunchMode.externalApplication, )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -69,7 +69,7 @@ class _MyHomePageState extends State { webViewConfiguration: const WebViewConfiguration( headers: {'my_header_key': 'my_header_value'}), )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -79,7 +79,7 @@ class _MyHomePageState extends State { mode: LaunchMode.inAppWebView, webViewConfiguration: const WebViewConfiguration(enableJavaScript: false), )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -89,7 +89,7 @@ class _MyHomePageState extends State { mode: LaunchMode.inAppWebView, webViewConfiguration: const WebViewConfiguration(enableDomStorage: false), )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } diff --git a/packages/url_launcher/url_launcher/example/pubspec.yaml b/packages/url_launcher/url_launcher/example/pubspec.yaml index 573dc0d9ed01..83900bfdef75 100644 --- a/packages/url_launcher/url_launcher/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher/lib/src/legacy_api.dart b/packages/url_launcher/url_launcher/lib/src/legacy_api.dart index 5bd5ef63d5b4..9f6d2dca001e 100644 --- a/packages/url_launcher/url_launcher/lib/src/legacy_api.dart +++ b/packages/url_launcher/url_launcher/lib/src/legacy_api.dart @@ -150,5 +150,4 @@ Future closeWebView() async { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 642b4b51e90c..e4f6d4c7c5c4 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.1.7 +version: 6.1.9 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart index 40336a090ab7..b2fde31d526d 100644 --- a/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart +++ b/packages/url_launcher/url_launcher/test/src/legacy_api_test.dart @@ -321,8 +321,6 @@ void main() { /// This removes the type information from a value so that it can be cast /// to another type even if that cast is redundant. -/// /// We use this so that APIs whose type have become more descriptive can still /// be used on the stable branch where they require a cast. -// TODO(ianh): Remove this once we roll stable in late 2021. Object? _anonymize(T? value) => value; diff --git a/packages/url_launcher/url_launcher_android/CHANGELOG.md b/packages/url_launcher/url_launcher_android/CHANGELOG.md index f2f65949e211..1062de50c4ca 100644 --- a/packages/url_launcher/url_launcher_android/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_android/CHANGELOG.md @@ -1,3 +1,11 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 6.0.23 + +* Updates code for stricter lint checks. + ## 6.0.22 * Updates code for new analysis options. diff --git a/packages/url_launcher/url_launcher_android/android/build.gradle b/packages/url_launcher/url_launcher_android/android/build.gradle index dbd68d99c1a2..63d81249ed8e 100644 --- a/packages/url_launcher/url_launcher_android/android/build.gradle +++ b/packages/url_launcher/url_launcher_android/android/build.gradle @@ -29,9 +29,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } @@ -51,7 +49,7 @@ android { dependencies { compileOnly 'androidx.annotation:annotation:1.2.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:4.8.0' + testImplementation 'org.mockito:mockito-core:5.1.1' testImplementation 'androidx.test:core:1.0.0' testImplementation 'org.robolectric:robolectric:4.3' } diff --git a/packages/url_launcher/url_launcher_android/example/android/gradle.properties b/packages/url_launcher/url_launcher_android/example/android/gradle.properties index a6738207fd15..e5611e4c7fa0 100644 --- a/packages/url_launcher/url_launcher_android/example/android/gradle.properties +++ b/packages/url_launcher/url_launcher_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/url_launcher/url_launcher_android/example/lib/main.dart b/packages/url_launcher/url_launcher_android/example/lib/main.dart index 68cf334d6656..7a77c86aef72 100644 --- a/packages/url_launcher/url_launcher_android/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_android/example/lib/main.dart @@ -63,7 +63,7 @@ class _MyHomePageState extends State { universalLinksOnly: false, headers: {}, )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -77,7 +77,7 @@ class _MyHomePageState extends State { universalLinksOnly: false, headers: {'my_header_key': 'my_header_value'}, )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -91,7 +91,7 @@ class _MyHomePageState extends State { universalLinksOnly: false, headers: {}, )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -105,7 +105,7 @@ class _MyHomePageState extends State { universalLinksOnly: false, headers: {}, )) { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } diff --git a/packages/url_launcher/url_launcher_android/example/pubspec.yaml b/packages/url_launcher/url_launcher_android/example/pubspec.yaml index 6c922c7a0f7d..33fc9f06ed63 100644 --- a/packages/url_launcher/url_launcher_android/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_android/pubspec.yaml b/packages/url_launcher/url_launcher_android/pubspec.yaml index d096b0ccc2cc..599274a95ebc 100644 --- a/packages/url_launcher/url_launcher_android/pubspec.yaml +++ b/packages/url_launcher/url_launcher_android/pubspec.yaml @@ -2,11 +2,11 @@ name: url_launcher_android description: Android implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.0.22 +version: 6.0.23 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart index b8ccc2cbbee7..18db61e0b9fa 100644 --- a/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart +++ b/packages/url_launcher/url_launcher_android/test/url_launcher_android_test.dart @@ -16,7 +16,9 @@ void main() { setUp(() { log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); // Return null explicitly instead of relying on the implicit null @@ -32,7 +34,9 @@ void main() { group('canLaunch', () { test('calls through', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); return true; }); @@ -59,7 +63,9 @@ void main() { test('checks a generic URL if an http URL returns false', () async { const String specificUrl = 'http://example.com/'; const String genericUrl = 'http://flutter.dev'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); return (methodCall.arguments as Map)['url'] != specificUrl; @@ -76,7 +82,9 @@ void main() { test('checks a generic URL if an https URL returns false', () async { const String specificUrl = 'https://example.com/'; const String genericUrl = 'https://flutter.dev'; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); return (methodCall.arguments as Map)['url'] != specificUrl; @@ -91,7 +99,9 @@ void main() { }); test('does not a generic URL if a non-web URL returns false', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); return false; }); @@ -288,3 +298,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher_ios/CHANGELOG.md b/packages/url_launcher/url_launcher_ios/CHANGELOG.md index cf018da4f59d..86546d45566d 100644 --- a/packages/url_launcher/url_launcher_ios/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_ios/CHANGELOG.md @@ -1,5 +1,10 @@ -## NEXT +## 6.1.0 +* Updates minimum Flutter version to 3.3 and iOS 11. + +## 6.0.18 + +* Updates code for stricter lint checks. * Updates minimum Flutter version to 2.10. ## 6.0.17 diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Flutter/AppFrameworkInfo.plist b/packages/url_launcher/url_launcher_ios/example/ios/Flutter/AppFrameworkInfo.plist index 3a9c234f96d4..9b41e7d87980 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/url_launcher/url_launcher_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 9.0 + 11.0 diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Podfile b/packages/url_launcher/url_launcher_ios/example/ios/Podfile index 3924e59aa0f9..ec43b513b0d1 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Podfile +++ b/packages/url_launcher/url_launcher_ios/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '9.0' +# platform :ios, '11.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj index 595f85d9a75b..d61abc724469 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -269,7 +269,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1100; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -339,10 +339,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -353,6 +355,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -517,7 +520,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -567,7 +570,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c5f1a9de4a30..ad0ebfab1b88 100644 --- a/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/url_launcher/url_launcher_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/packages/url_launcher/url_launcher_ios/example/lib/main.dart b/packages/url_launcher/url_launcher_ios/example/lib/main.dart index 6e8ce7431a66..f01624ff87c6 100644 --- a/packages/url_launcher/url_launcher_ios/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_ios/example/lib/main.dart @@ -14,7 +14,7 @@ void main() { } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override Widget build(BuildContext context) { @@ -29,7 +29,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key, required this.title}) : super(key: key); + const MyHomePage({super.key, required this.title}); final String title; @override @@ -53,7 +53,7 @@ class _MyHomePageState extends State { headers: {'my_header_key': 'my_header_value'}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -70,7 +70,7 @@ class _MyHomePageState extends State { headers: {'my_header_key': 'my_header_value'}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -87,7 +87,7 @@ class _MyHomePageState extends State { headers: {}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -104,7 +104,7 @@ class _MyHomePageState extends State { headers: {}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } @@ -155,7 +155,7 @@ class _MyHomePageState extends State { headers: {}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } diff --git a/packages/url_launcher/url_launcher_ios/example/pubspec.yaml b/packages/url_launcher/url_launcher_ios/example/pubspec.yaml index 9a134c747fa4..21b191ad0cce 100644 --- a/packages/url_launcher/url_launcher_ios/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/example/pubspec.yaml @@ -3,8 +3,8 @@ description: Demonstrates how to use the url_launcher plugin. publish_to: none environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + sdk: '>=2.18.0 <3.0.0' + flutter: ">=3.3.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m index af720c87b8b2..375d5e2a2354 100644 --- a/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m +++ b/packages/url_launcher/url_launcher_ios/ios/Classes/FLTURLLauncherPlugin.m @@ -6,7 +6,6 @@ #import "FLTURLLauncherPlugin.h" -API_AVAILABLE(ios(9.0)) @interface FLTURLLaunchSession : NSObject @property(copy, nonatomic) FlutterResult flutterResult; @@ -30,7 +29,7 @@ - (instancetype)initWithUrl:url withFlutterResult:result { } - (void)safariViewController:(SFSafariViewController *)controller - didCompleteInitialLoad:(BOOL)didLoadSuccessfully API_AVAILABLE(ios(9.0)) { + didCompleteInitialLoad:(BOOL)didLoadSuccessfully { if (didLoadSuccessfully) { self.flutterResult(@YES); } else { @@ -41,7 +40,7 @@ - (void)safariViewController:(SFSafariViewController *)controller } } -- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller API_AVAILABLE(ios(9.0)) { +- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller { [controller dismissViewControllerAnimated:YES completion:nil]; self.didFinish(); } @@ -52,7 +51,6 @@ - (void)close { @end -API_AVAILABLE(ios(9.0)) @interface FLTURLLauncherPlugin () @property(strong, nonatomic) FLTURLLaunchSession *currentSession; @@ -99,24 +97,16 @@ - (void)launchURL:(NSString *)urlString NSURL *url = [NSURL URLWithString:urlString]; UIApplication *application = [UIApplication sharedApplication]; - if (@available(iOS 10.0, *)) { - NSNumber *universalLinksOnly = call.arguments[@"universalLinksOnly"] ?: @0; - NSDictionary *options = @{UIApplicationOpenURLOptionUniversalLinksOnly : universalLinksOnly}; - [application openURL:url - options:options - completionHandler:^(BOOL success) { - result(@(success)); - }]; - } else { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - BOOL success = [application openURL:url]; -#pragma clang diagnostic pop - result(@(success)); - } + NSNumber *universalLinksOnly = call.arguments[@"universalLinksOnly"] ?: @0; + NSDictionary *options = @{UIApplicationOpenURLOptionUniversalLinksOnly : universalLinksOnly}; + [application openURL:url + options:options + completionHandler:^(BOOL success) { + result(@(success)); + }]; } -- (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result API_AVAILABLE(ios(9.0)) { +- (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result { NSURL *url = [NSURL URLWithString:urlString]; self.currentSession = [[FLTURLLaunchSession alloc] initWithUrl:url withFlutterResult:result]; __weak typeof(self) weakSelf = self; @@ -128,7 +118,7 @@ - (void)launchURLInVC:(NSString *)urlString result:(FlutterResult)result API_AVA completion:nil]; } -- (void)closeWebViewWithResult:(FlutterResult)result API_AVAILABLE(ios(9.0)) { +- (void)closeWebViewWithResult:(FlutterResult)result { if (self.currentSession != nil) { [self.currentSession close]; } diff --git a/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec b/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec index 1c0e81964252..9c265694018e 100644 --- a/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec +++ b/packages/url_launcher/url_launcher_ios/ios/url_launcher_ios.podspec @@ -16,7 +16,6 @@ A Flutter plugin for making the underlying platform (Android or iOS) launch a UR s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - - s.platform = :ios, '9.0' + s.platform = :ios, '11.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/url_launcher/url_launcher_ios/pubspec.yaml b/packages/url_launcher/url_launcher_ios/pubspec.yaml index 5e06a80b4cfe..5a5c4bdc0514 100644 --- a/packages/url_launcher/url_launcher_ios/pubspec.yaml +++ b/packages/url_launcher/url_launcher_ios/pubspec.yaml @@ -2,11 +2,11 @@ name: url_launcher_ios description: iOS implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 6.0.17 +version: 6.1.0 environment: - sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + sdk: '>=2.18.0 <3.0.0' + flutter: ">=3.3.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart index 8fad5807bddb..34dac1c4f925 100644 --- a/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart +++ b/packages/url_launcher/url_launcher_ios/test/url_launcher_ios_test.dart @@ -14,7 +14,9 @@ void main() { const MethodChannel channel = MethodChannel('plugins.flutter.io/url_launcher_ios'); final List log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); // Return null explicitly instead of relying on the implicit null @@ -206,3 +208,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher_linux/CHANGELOG.md b/packages/url_launcher/url_launcher_linux/CHANGELOG.md index 69d8b24ddf6f..3d955871c8c8 100644 --- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 3.0.2 + +* Updates code for stricter lint checks. * Updates minimum Flutter version to 2.10. ## 3.0.1 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 0b985e78ac0d..bbe651ea05de 100644 --- a/packages/url_launcher/url_launcher_linux/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_linux/example/lib/main.dart @@ -50,7 +50,7 @@ class _MyHomePageState extends State { headers: {}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } diff --git a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml index 17effeb1ffcb..ba738806af38 100644 --- a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml index 99b237506c60..e455ab83bef5 100644 --- a/packages/url_launcher/url_launcher_linux/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml @@ -2,11 +2,11 @@ name: url_launcher_linux description: Linux implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_linux issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 3.0.1 +version: 3.0.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart b/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart index 7a4399dd4e6c..4e62cc446199 100644 --- a/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart +++ b/packages/url_launcher/url_launcher_linux/test/url_launcher_linux_test.dart @@ -14,7 +14,9 @@ void main() { const MethodChannel channel = MethodChannel('plugins.flutter.io/url_launcher_linux'); final List log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); // Return null explicitly instead of relying on the implicit null @@ -142,3 +144,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md index 7386ecced865..eb42ba920e23 100644 --- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 3.0.2 + +* Updates code for stricter lint checks. * Updates minimum Flutter version to 2.10. ## 3.0.1 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 0b985e78ac0d..bbe651ea05de 100644 --- a/packages/url_launcher/url_launcher_macos/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_macos/example/lib/main.dart @@ -50,7 +50,7 @@ class _MyHomePageState extends State { headers: {}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } diff --git a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml index 3b802ea229ba..688cac3a6b0e 100644 --- a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_macos/pubspec.yaml b/packages/url_launcher/url_launcher_macos/pubspec.yaml index eaf210a367b6..2ec915fc2ddb 100644 --- a/packages/url_launcher/url_launcher_macos/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/pubspec.yaml @@ -2,11 +2,11 @@ name: url_launcher_macos description: macOS implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_macos issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 3.0.1 +version: 3.0.2 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart b/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart index 0a28aea678c3..26011fa6779a 100644 --- a/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart +++ b/packages/url_launcher/url_launcher_macos/test/url_launcher_macos_test.dart @@ -14,7 +14,9 @@ void main() { const MethodChannel channel = MethodChannel('plugins.flutter.io/url_launcher_macos'); final List log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); // Return null explicitly instead of relying on the implicit null @@ -142,3 +144,9 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md index d45ca36e3906..fecd2a45c4cb 100644 --- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.1.1 * Updates imports for `prefer_relative_imports`. 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 da8aa1570bad..bddadad893a7 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart @@ -109,5 +109,4 @@ Future pushRouteNameToFramework(Object? _, String routeName) { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml index 4364e116c508..ab37dc32eedd 100644 --- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml +++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 2.1.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: 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 c8ec08c53095..9ccdd84ae890 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 @@ -48,7 +48,9 @@ void main() { const MethodChannel channel = MethodChannel('plugins.flutter.io/url_launcher'); final List log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { log.add(methodCall); // Return null explicitly instead of relying on the implicit null @@ -323,3 +325,9 @@ class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform { @override final LinkDelegate? linkDelegate = null; } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index 5454338bde51..51b2de90b88a 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,5 +1,10 @@ ## NEXT +* Updates minimum Flutter version to 3.0. + +## 2.0.14 + +* Updates code for stricter lint checks. * Updates minimum Flutter version to 2.10. ## 2.0.13 diff --git a/packages/url_launcher/url_launcher_web/example/pubspec.yaml b/packages/url_launcher/url_launcher_web/example/pubspec.yaml index f972b2857ecf..ca1b0d6634a7 100644 --- a/packages/url_launcher/url_launcher_web/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: 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 112d07ea7571..78c049c03def 100644 --- a/packages/url_launcher/url_launcher_web/lib/src/link.dart +++ b/packages/url_launcher/url_launcher_web/lib/src/link.dart @@ -247,9 +247,13 @@ class LinkViewController extends PlatformViewController { return '_self'; case LinkTarget.blank: return '_blank'; - default: - throw Exception('Unknown LinkTarget value $target.'); } + // The enum comes from a different package, which could get a new value at + // any time, so provide a fallback that ensures this won't break when used + // with a version that contains new values. This is deliberately outside + // the switch rather than a `default` so that the linter will flag the + // switch as needing an update. + return '_self'; } @override diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index 6d4c80689427..8c8214ef6e4b 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -2,11 +2,11 @@ name: url_launcher_web description: Web platform implementation of url_launcher repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 2.0.13 +version: 2.0.14 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index a5952feb4978..abb3ab10db57 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,5 +1,11 @@ -## NEXT +## 3.0.3 +* Converts internal implentation to Pigeon. +* Updates minimum Flutter version to 3.0. + +## 3.0.2 + +* Updates code for stricter lint checks. * Updates minimum Flutter version to 2.10. ## 3.0.1 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 0b985e78ac0d..bbe651ea05de 100644 --- a/packages/url_launcher/url_launcher_windows/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_windows/example/lib/main.dart @@ -50,7 +50,7 @@ class _MyHomePageState extends State { headers: {}, ); } else { - throw 'Could not launch $url'; + throw Exception('Could not launch $url'); } } diff --git a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml index 966d32c779e8..231d3d0848bc 100644 --- a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_windows/lib/src/messages.g.dart b/packages/url_launcher/url_launcher_windows/lib/src/messages.g.dart new file mode 100644 index 000000000000..a1d46c11267d --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/lib/src/messages.g.dart @@ -0,0 +1,71 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v5.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +class UrlLauncherApi { + /// Constructor for [UrlLauncherApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + UrlLauncherApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + + Future canLaunchUrl(String arg_url) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_url]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + Future launchUrl(String arg_url) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.UrlLauncherApi.launchUrl', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_url]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } +} diff --git a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart index b0ee8cb1a0b4..41c403e56f8e 100644 --- a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart +++ b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart @@ -2,17 +2,21 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - -import 'package:flutter/services.dart'; +import 'package:flutter/foundation.dart'; import 'package:url_launcher_platform_interface/link.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; -const MethodChannel _channel = - MethodChannel('plugins.flutter.io/url_launcher_windows'); +import 'src/messages.g.dart'; /// An implementation of [UrlLauncherPlatform] for Windows. class UrlLauncherWindows extends UrlLauncherPlatform { + /// Creates a new plugin implementation instance. + UrlLauncherWindows({ + @visibleForTesting UrlLauncherApi? api, + }) : _hostApi = api ?? UrlLauncherApi(); + + final UrlLauncherApi _hostApi; + /// Registers this class as the default instance of [UrlLauncherPlatform]. static void registerWith() { UrlLauncherPlatform.instance = UrlLauncherWindows(); @@ -23,10 +27,7 @@ class UrlLauncherWindows extends UrlLauncherPlatform { @override Future canLaunch(String url) { - return _channel.invokeMethod( - 'canLaunch', - {'url': url}, - ).then((bool? value) => value ?? false); + return _hostApi.canLaunchUrl(url); } @override @@ -39,16 +40,9 @@ class UrlLauncherWindows extends UrlLauncherPlatform { required bool universalLinksOnly, required Map headers, String? webOnlyWindowName, - }) { - return _channel.invokeMethod( - 'launch', - { - 'url': url, - 'enableJavaScript': enableJavaScript, - 'enableDomStorage': enableDomStorage, - 'universalLinksOnly': universalLinksOnly, - 'headers': headers, - }, - ).then((bool? value) => value ?? false); + }) async { + await _hostApi.launchUrl(url); + // Failure is handled via a PlatformException from `launchUrl`. + return true; } } diff --git a/packages/url_launcher/url_launcher_windows/pigeons/copyright.txt b/packages/url_launcher/url_launcher_windows/pigeons/copyright.txt new file mode 100644 index 000000000000..1236b63caf3a --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/pigeons/copyright.txt @@ -0,0 +1,3 @@ +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. diff --git a/packages/url_launcher/url_launcher_windows/pigeons/messages.dart b/packages/url_launcher/url_launcher_windows/pigeons/messages.dart new file mode 100644 index 000000000000..9607cdffc686 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/pigeons/messages.dart @@ -0,0 +1,18 @@ +// 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:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + cppOptions: CppOptions(namespace: 'url_launcher_windows'), + cppHeaderOut: 'windows/messages.g.h', + cppSourceOut: 'windows/messages.g.cpp', + copyrightHeader: 'pigeons/copyright.txt', +)) +@HostApi(dartHostTestHandler: 'TestUrlLauncherApi') +abstract class UrlLauncherApi { + bool canLaunchUrl(String url); + void launchUrl(String url); +} diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index b35b62e1d82e..de4f5edd69eb 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -2,11 +2,11 @@ name: url_launcher_windows description: Windows implementation of the url_launcher plugin. repository: https://github.com/flutter/plugins/tree/main/packages/url_launcher/url_launcher_windows issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 -version: 3.0.1 +version: 3.0.3 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: @@ -24,4 +24,5 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + pigeon: ^5.0.1 test: ^1.16.3 diff --git a/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart b/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart index 8b55b29bb530..7f48f64fa92c 100644 --- a/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart +++ b/packages/url_launcher/url_launcher_windows/test/url_launcher_windows_test.dart @@ -5,140 +5,101 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +import 'package:url_launcher_windows/src/messages.g.dart'; import 'package:url_launcher_windows/url_launcher_windows.dart'; void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$UrlLauncherWindows', () { - const MethodChannel channel = - MethodChannel('plugins.flutter.io/url_launcher_windows'); - 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; - }); + late _FakeUrlLauncherApi api; + late UrlLauncherWindows plugin; - test('registers instance', () { - UrlLauncherWindows.registerWith(); - expect(UrlLauncherPlatform.instance, isA()); - }); + setUp(() { + api = _FakeUrlLauncherApi(); + plugin = UrlLauncherWindows(api: api); + }); - tearDown(() { - log.clear(); - }); + test('registers instance', () { + UrlLauncherWindows.registerWith(); + expect(UrlLauncherPlatform.instance, isA()); + }); - test('canLaunch', () async { - final UrlLauncherWindows launcher = UrlLauncherWindows(); - await launcher.canLaunch('http://example.com/'); - expect( - log, - [ - isMethodCall('canLaunch', arguments: { - 'url': 'http://example.com/', - }) - ], - ); - }); + group('canLaunch', () { + test('handles true', () async { + api.canLaunch = true; - test('canLaunch should return false if platform returns null', () async { - final UrlLauncherWindows launcher = UrlLauncherWindows(); - final bool canLaunch = await launcher.canLaunch('http://example.com/'); + final bool result = await plugin.canLaunch('http://example.com/'); - expect(canLaunch, false); + expect(result, isTrue); + expect(api.argument, 'http://example.com/'); }); - test('launch', () async { - final UrlLauncherWindows launcher = UrlLauncherWindows(); - await launcher.launch( - 'http://example.com/', - useSafariVC: true, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: const {}, - ); - expect( - log, - [ - isMethodCall('launch', arguments: { - 'url': 'http://example.com/', - 'enableJavaScript': false, - 'enableDomStorage': false, - 'universalLinksOnly': false, - 'headers': {}, - }) - ], - ); - }); + test('handles false', () async { + api.canLaunch = false; - test('launch with headers', () async { - final UrlLauncherWindows launcher = UrlLauncherWindows(); - await launcher.launch( - 'http://example.com/', - useSafariVC: true, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: const {'key': 'value'}, - ); - expect( - log, - [ - isMethodCall('launch', arguments: { - 'url': 'http://example.com/', - 'enableJavaScript': false, - 'enableDomStorage': false, - 'universalLinksOnly': false, - 'headers': {'key': 'value'}, - }) - ], - ); + final bool result = await plugin.canLaunch('http://example.com/'); + + expect(result, isFalse); + expect(api.argument, 'http://example.com/'); }); + }); + + group('launch', () { + test('handles success', () async { + api.canLaunch = true; - test('launch universal links only', () async { - final UrlLauncherWindows launcher = UrlLauncherWindows(); - await launcher.launch( - 'http://example.com/', - useSafariVC: false, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: true, - headers: const {}, - ); expect( - log, - [ - isMethodCall('launch', arguments: { - 'url': 'http://example.com/', - 'enableJavaScript': false, - 'enableDomStorage': false, - 'universalLinksOnly': true, - 'headers': {}, - }) - ], - ); + plugin.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ), + completes); + expect(api.argument, 'http://example.com/'); }); - test('launch should return false if platform returns null', () async { - final UrlLauncherWindows launcher = UrlLauncherWindows(); - final bool launched = await launcher.launch( - 'http://example.com/', - useSafariVC: true, - useWebView: false, - enableJavaScript: false, - enableDomStorage: false, - universalLinksOnly: false, - headers: const {}, - ); - - expect(launched, false); + test('handles failure', () async { + api.canLaunch = false; + + await expectLater( + plugin.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ), + throwsA(isA())); + expect(api.argument, 'http://example.com/'); }); }); } + +class _FakeUrlLauncherApi implements UrlLauncherApi { + /// The argument that was passed to an API call. + String? argument; + + /// Controls the behavior of the fake implementations. + /// + /// - [canLaunchUrl] returns this value. + /// - [launchUrl] throws if this is false. + bool canLaunch = false; + + @override + Future canLaunchUrl(String url) async { + argument = url; + return canLaunch; + } + + @override + Future launchUrl(String url) async { + argument = url; + if (!canLaunch) { + throw PlatformException(code: 'Failed'); + } + } +} diff --git a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt index a4185acff6a1..a34bcb3d35da 100644 --- a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt +++ b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt @@ -5,6 +5,8 @@ project(${PROJECT_NAME} LANGUAGES CXX) set(PLUGIN_NAME "${PROJECT_NAME}_plugin") list(APPEND PLUGIN_SOURCES + "messages.g.cpp" + "messages.g.h" "system_apis.cpp" "system_apis.h" "url_launcher_plugin.cpp" diff --git a/packages/url_launcher/url_launcher_windows/windows/messages.g.cpp b/packages/url_launcher/url_launcher_windows/windows/messages.g.cpp new file mode 100644 index 000000000000..eb1cf792931f --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/messages.g.cpp @@ -0,0 +1,113 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v5.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#undef _HAS_EXCEPTIONS + +#include "messages.g.h" + +#include +#include +#include +#include + +#include +#include +#include + +namespace url_launcher_windows { + +/// The codec used by UrlLauncherApi. +const flutter::StandardMessageCodec& UrlLauncherApi::GetCodec() { + return flutter::StandardMessageCodec::GetInstance( + &flutter::StandardCodecSerializer::GetInstance()); +} + +// Sets up an instance of `UrlLauncherApi` to handle messages through the +// `binary_messenger`. +void UrlLauncherApi::SetUp(flutter::BinaryMessenger* binary_messenger, + UrlLauncherApi* api) { + { + auto channel = + std::make_unique>( + binary_messenger, "dev.flutter.pigeon.UrlLauncherApi.canLaunchUrl", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_url_arg = args.at(0); + if (encodable_url_arg.IsNull()) { + reply(WrapError("url_arg unexpectedly null.")); + return; + } + const auto& url_arg = std::get(encodable_url_arg); + ErrorOr output = api->CanLaunchUrl(url_arg); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + flutter::EncodableList wrapped; + wrapped.push_back( + flutter::EncodableValue(std::move(output).TakeValue())); + reply(flutter::EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = + std::make_unique>( + binary_messenger, "dev.flutter.pigeon.UrlLauncherApi.launchUrl", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const flutter::EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_url_arg = args.at(0); + if (encodable_url_arg.IsNull()) { + reply(WrapError("url_arg unexpectedly null.")); + return; + } + const auto& url_arg = std::get(encodable_url_arg); + std::optional output = api->LaunchUrl(url_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + flutter::EncodableList wrapped; + wrapped.push_back(flutter::EncodableValue()); + reply(flutter::EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } +} + +flutter::EncodableValue UrlLauncherApi::WrapError( + std::string_view error_message) { + return flutter::EncodableValue(flutter::EncodableList{ + flutter::EncodableValue(std::string(error_message)), + flutter::EncodableValue("Error"), flutter::EncodableValue()}); +} +flutter::EncodableValue UrlLauncherApi::WrapError(const FlutterError& error) { + return flutter::EncodableValue(flutter::EncodableList{ + flutter::EncodableValue(error.message()), + flutter::EncodableValue(error.code()), error.details()}); +} + +} // namespace url_launcher_windows diff --git a/packages/url_launcher/url_launcher_windows/windows/messages.g.h b/packages/url_launcher/url_launcher_windows/windows/messages.g.h new file mode 100644 index 000000000000..cb8e95f8d065 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/messages.g.h @@ -0,0 +1,86 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v5.0.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +#ifndef PIGEON_H_ +#define PIGEON_H_ +#include +#include +#include +#include + +#include +#include +#include + +namespace url_launcher_windows { + +// Generated class from Pigeon. + +class FlutterError { + public: + explicit FlutterError(const std::string& code) : code_(code) {} + explicit FlutterError(const std::string& code, const std::string& message) + : code_(code), message_(message) {} + explicit FlutterError(const std::string& code, const std::string& message, + const flutter::EncodableValue& details) + : code_(code), message_(message), details_(details) {} + + const std::string& code() const { return code_; } + const std::string& message() const { return message_; } + const flutter::EncodableValue& details() const { return details_; } + + private: + std::string code_; + std::string message_; + flutter::EncodableValue details_; +}; + +template +class ErrorOr { + public: + ErrorOr(const T& rhs) { new (&v_) T(rhs); } + ErrorOr(const T&& rhs) { v_ = std::move(rhs); } + ErrorOr(const FlutterError& rhs) { new (&v_) FlutterError(rhs); } + ErrorOr(const FlutterError&& rhs) { v_ = std::move(rhs); } + + bool has_error() const { return std::holds_alternative(v_); } + const T& value() const { return std::get(v_); }; + const FlutterError& error() const { return std::get(v_); }; + + private: + friend class UrlLauncherApi; + ErrorOr() = default; + T TakeValue() && { return std::get(std::move(v_)); } + + std::variant v_; +}; + +// Generated interface from Pigeon that represents a handler of messages from +// Flutter. +class UrlLauncherApi { + public: + UrlLauncherApi(const UrlLauncherApi&) = delete; + UrlLauncherApi& operator=(const UrlLauncherApi&) = delete; + virtual ~UrlLauncherApi(){}; + virtual ErrorOr CanLaunchUrl(const std::string& url) = 0; + virtual std::optional LaunchUrl(const std::string& url) = 0; + + // The codec used by UrlLauncherApi. + static const flutter::StandardMessageCodec& GetCodec(); + // Sets up an instance of `UrlLauncherApi` to handle messages through the + // `binary_messenger`. + static void SetUp(flutter::BinaryMessenger* binary_messenger, + UrlLauncherApi* api); + static flutter::EncodableValue WrapError(std::string_view error_message); + static flutter::EncodableValue WrapError(const FlutterError& error); + + protected: + UrlLauncherApi() = default; +}; + +} // namespace url_launcher_windows + +#endif // PIGEON_H_ diff --git a/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp b/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp index abd690b6e47f..cde95ee1b399 100644 --- a/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp +++ b/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp @@ -5,7 +5,7 @@ #include -namespace url_launcher_plugin { +namespace url_launcher_windows { SystemApis::SystemApis() {} @@ -35,4 +35,4 @@ HINSTANCE SystemApisImpl::ShellExecuteW(HWND hwnd, LPCWSTR operation, show_flags); } -} // namespace url_launcher_plugin +} // namespace url_launcher_windows diff --git a/packages/url_launcher/url_launcher_windows/windows/system_apis.h b/packages/url_launcher/url_launcher_windows/windows/system_apis.h index 7b56704d8e04..c56c4100180b 100644 --- a/packages/url_launcher/url_launcher_windows/windows/system_apis.h +++ b/packages/url_launcher/url_launcher_windows/windows/system_apis.h @@ -3,7 +3,7 @@ // found in the LICENSE file. #include -namespace url_launcher_plugin { +namespace url_launcher_windows { // An interface wrapping system APIs used by the plugin, for mocking. class SystemApis { @@ -53,4 +53,4 @@ class SystemApisImpl : public SystemApis { int show_flags); }; -} // namespace url_launcher_plugin +} // namespace url_launcher_windows diff --git a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp index 191d51a0caa8..9dd2be5347b5 100644 --- a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp +++ b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp @@ -9,11 +9,13 @@ #include #include +#include #include +#include "messages.g.h" #include "url_launcher_plugin.h" -namespace url_launcher_plugin { +namespace url_launcher_windows { namespace test { namespace { @@ -42,30 +44,10 @@ class MockSystemApis : public SystemApis { (override)); }; -class MockMethodResult : public flutter::MethodResult<> { - public: - MOCK_METHOD(void, SuccessInternal, (const EncodableValue* result), - (override)); - MOCK_METHOD(void, ErrorInternal, - (const std::string& error_code, const std::string& error_message, - const EncodableValue* details), - (override)); - MOCK_METHOD(void, NotImplementedInternal, (), (override)); -}; - -std::unique_ptr CreateArgumentsWithUrl(const std::string& url) { - EncodableMap args = { - {EncodableValue("url"), EncodableValue(url)}, - }; - return std::make_unique(args); -} - } // namespace TEST(UrlLauncherPlugin, CanLaunchSuccessTrue) { std::unique_ptr system = std::make_unique(); - std::unique_ptr result = - std::make_unique(); // Return success values from the registery commands. HKEY fake_key = reinterpret_cast(1); @@ -73,20 +55,16 @@ TEST(UrlLauncherPlugin, CanLaunchSuccessTrue) { .WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS))); EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_SUCCESS)); EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS)); - // Expect a success response. - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); UrlLauncherPlugin plugin(std::move(system)); - plugin.HandleMethodCall( - flutter::MethodCall("canLaunch", - CreateArgumentsWithUrl("https://some.url.com")), - std::move(result)); + ErrorOr result = plugin.CanLaunchUrl("https://some.url.com"); + + ASSERT_FALSE(result.has_error()); + EXPECT_TRUE(result.value()); } TEST(UrlLauncherPlugin, CanLaunchQueryFailure) { std::unique_ptr system = std::make_unique(); - std::unique_ptr result = - std::make_unique(); // Return success values from the registery commands, except for the query, // to simulate a scheme that is in the registry, but has no URL handler. @@ -95,68 +73,52 @@ TEST(UrlLauncherPlugin, CanLaunchQueryFailure) { .WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS))); EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_FILE_NOT_FOUND)); EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS)); - // Expect a success response. - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); UrlLauncherPlugin plugin(std::move(system)); - plugin.HandleMethodCall( - flutter::MethodCall("canLaunch", - CreateArgumentsWithUrl("https://some.url.com")), - std::move(result)); + ErrorOr result = plugin.CanLaunchUrl("https://some.url.com"); + + ASSERT_FALSE(result.has_error()); + EXPECT_FALSE(result.value()); } TEST(UrlLauncherPlugin, CanLaunchHandlesOpenFailure) { std::unique_ptr system = std::make_unique(); - std::unique_ptr result = - std::make_unique(); // Return failure for opening. EXPECT_CALL(*system, RegOpenKeyExW).WillOnce(Return(ERROR_BAD_PATHNAME)); - // Expect a success response. - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); UrlLauncherPlugin plugin(std::move(system)); - plugin.HandleMethodCall( - flutter::MethodCall("canLaunch", - CreateArgumentsWithUrl("https://some.url.com")), - std::move(result)); + ErrorOr result = plugin.CanLaunchUrl("https://some.url.com"); + + ASSERT_FALSE(result.has_error()); + EXPECT_FALSE(result.value()); } TEST(UrlLauncherPlugin, LaunchSuccess) { std::unique_ptr system = std::make_unique(); - std::unique_ptr result = - std::make_unique(); // Return a success value (>32) from launching. EXPECT_CALL(*system, ShellExecuteW) .WillOnce(Return(reinterpret_cast(33))); - // Expect a success response. - EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); UrlLauncherPlugin plugin(std::move(system)); - plugin.HandleMethodCall( - flutter::MethodCall("launch", - CreateArgumentsWithUrl("https://some.url.com")), - std::move(result)); + std::optional error = plugin.LaunchUrl("https://some.url.com"); + + EXPECT_FALSE(error.has_value()); } TEST(UrlLauncherPlugin, LaunchReportsFailure) { std::unique_ptr system = std::make_unique(); - std::unique_ptr result = - std::make_unique(); // Return a faile value (<=32) from launching. EXPECT_CALL(*system, ShellExecuteW) .WillOnce(Return(reinterpret_cast(32))); - // Expect an error response. - EXPECT_CALL(*result, ErrorInternal); UrlLauncherPlugin plugin(std::move(system)); - plugin.HandleMethodCall( - flutter::MethodCall("launch", - CreateArgumentsWithUrl("https://some.url.com")), - std::move(result)); + std::optional error = plugin.LaunchUrl("https://some.url.com"); + + EXPECT_TRUE(error.has_value()); } } // namespace test -} // namespace url_launcher_plugin +} // namespace url_launcher_windows diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp index d5f201219c75..1dfee16c4445 100644 --- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp @@ -13,7 +13,9 @@ #include #include -namespace url_launcher_plugin { +#include "messages.g.h" + +namespace url_launcher_windows { namespace { @@ -62,18 +64,9 @@ std::string GetUrlArgument(const flutter::MethodCall<>& method_call) { // static void UrlLauncherPlugin::RegisterWithRegistrar( flutter::PluginRegistrar* registrar) { - auto channel = std::make_unique>( - registrar->messenger(), "plugins.flutter.io/url_launcher_windows", - &flutter::StandardMethodCodec::GetInstance()); - std::unique_ptr plugin = std::make_unique(); - - channel->SetMethodCallHandler( - [plugin_pointer = plugin.get()](const auto& call, auto result) { - plugin_pointer->HandleMethodCall(call, std::move(result)); - }); - + UrlLauncherApi::SetUp(registrar->messenger(), plugin.get()); registrar->AddPlugin(std::move(plugin)); } @@ -85,37 +78,7 @@ UrlLauncherPlugin::UrlLauncherPlugin(std::unique_ptr system_apis) UrlLauncherPlugin::~UrlLauncherPlugin() = default; -void UrlLauncherPlugin::HandleMethodCall( - const flutter::MethodCall<>& method_call, - std::unique_ptr> result) { - if (method_call.method_name().compare("launch") == 0) { - std::string url = GetUrlArgument(method_call); - if (url.empty()) { - result->Error("argument_error", "No URL provided"); - return; - } - - std::optional error = LaunchUrl(url); - if (error) { - result->Error("open_error", error.value()); - return; - } - result->Success(EncodableValue(true)); - } else if (method_call.method_name().compare("canLaunch") == 0) { - std::string url = GetUrlArgument(method_call); - if (url.empty()) { - result->Error("argument_error", "No URL provided"); - return; - } - - bool can_launch = CanLaunchUrl(url); - result->Success(EncodableValue(can_launch)); - } else { - result->NotImplemented(); - } -} - -bool UrlLauncherPlugin::CanLaunchUrl(const std::string& url) { +ErrorOr UrlLauncherPlugin::CanLaunchUrl(const std::string& url) { size_t separator_location = url.find(":"); if (separator_location == std::string::npos) { return false; @@ -134,7 +97,7 @@ bool UrlLauncherPlugin::CanLaunchUrl(const std::string& url) { return has_handler; } -std::optional UrlLauncherPlugin::LaunchUrl( +std::optional UrlLauncherPlugin::LaunchUrl( const std::string& url) { std::wstring url_wide = Utf16FromUtf8(url); @@ -147,9 +110,9 @@ std::optional UrlLauncherPlugin::LaunchUrl( std::ostringstream error_message; error_message << "Failed to open " << url << ": ShellExecute error code " << status; - return std::optional(error_message.str()); + return FlutterError("open_error", error_message.str()); } return std::nullopt; } -} // namespace url_launcher_plugin +} // namespace url_launcher_windows diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h index 45e70e5fc067..e51cde67ab79 100644 --- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h @@ -10,11 +10,12 @@ #include #include +#include "messages.g.h" #include "system_apis.h" -namespace url_launcher_plugin { +namespace url_launcher_windows { -class UrlLauncherPlugin : public flutter::Plugin { +class UrlLauncherPlugin : public flutter::Plugin, public UrlLauncherApi { public: static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar); @@ -31,18 +32,12 @@ class UrlLauncherPlugin : public flutter::Plugin { UrlLauncherPlugin(const UrlLauncherPlugin&) = delete; UrlLauncherPlugin& operator=(const UrlLauncherPlugin&) = delete; - // Called when a method is called on the plugin channel. - void HandleMethodCall(const flutter::MethodCall<>& method_call, - std::unique_ptr> result); + // UrlLauncherApi: + ErrorOr CanLaunchUrl(const std::string& url) override; + std::optional LaunchUrl(const std::string& url) override; private: - // Returns whether or not the given URL has a registered handler. - bool CanLaunchUrl(const std::string& url); - - // Attempts to launch the given URL. On failure, returns an error string. - std::optional LaunchUrl(const std::string& url); - std::unique_ptr system_apis_; }; -} // namespace url_launcher_plugin +} // namespace url_launcher_windows diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp index 05de586d8fe0..726709386fa6 100644 --- a/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp @@ -9,7 +9,7 @@ void UrlLauncherWindowsRegisterWithRegistrar( FlutterDesktopPluginRegistrarRef registrar) { - url_launcher_plugin::UrlLauncherPlugin::RegisterWithRegistrar( + url_launcher_windows::UrlLauncherPlugin::RegisterWithRegistrar( flutter::PluginRegistrarManager::GetInstance() ->GetRegistrar(registrar)); } diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index c91694627bb8..eed3b6bc2346 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,15 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + +## 2.5.1 + +* Updates code for stricter lint checks. + +## 2.5.0 + +* Exposes `VideoScrubber` so it can be used to make custom video progress indicators + ## 2.4.10 * Adds compatibilty with version 6.0 of the platform interface. diff --git a/packages/video_player/video_player/example/android/app/build.gradle b/packages/video_player/video_player/example/android/app/build.gradle index 338eeb8944f7..8b2086b6c05e 100644 --- a/packages/video_player/video_player/example/android/app/build.gradle +++ b/packages/video_player/video_player/example/android/app/build.gradle @@ -60,7 +60,7 @@ flutter { dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.8.1' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:5.0.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/packages/video_player/video_player/example/android/gradle.properties b/packages/video_player/video_player/example/android/gradle.properties index a6738207fd15..e5611e4c7fa0 100644 --- a/packages/video_player/video_player/example/android/gradle.properties +++ b/packages/video_player/video_player/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/video_player/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml index 7b6aa09329fa..0b30e9fb01e7 100644 --- a/packages/video_player/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index 76f865aa7359..5720e2d9d136 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -697,17 +697,13 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { - switch (state) { - case AppLifecycleState.paused: - _wasPlayingBeforePause = _controller.value.isPlaying; - _controller.pause(); - break; - case AppLifecycleState.resumed: - if (_wasPlayingBeforePause) { - _controller.play(); - } - break; - default: + if (state == AppLifecycleState.paused) { + _wasPlayingBeforePause = _controller.value.isPlaying; + _controller.pause(); + } else if (state == AppLifecycleState.resumed) { + if (_wasPlayingBeforePause) { + _controller.play(); + } } } @@ -835,20 +831,29 @@ class VideoProgressColors { final Color backgroundColor; } -class _VideoScrubber extends StatefulWidget { - const _VideoScrubber({ +/// A scrubber to control [VideoPlayerController]s +class VideoScrubber extends StatefulWidget { + /// Create a [VideoScrubber] handler with the given [child]. + /// + /// [controller] is the [VideoPlayerController] that will be controlled by + /// this scrubber. + const VideoScrubber({ + Key? key, required this.child, required this.controller, - }); + }) : super(key: key); + /// The widget that will be displayed inside the gesture detector. final Widget child; + + /// The [VideoPlayerController] that will be controlled by this scrubber. final VideoPlayerController controller; @override - _VideoScrubberState createState() => _VideoScrubberState(); + State createState() => _VideoScrubberState(); } -class _VideoScrubberState extends State<_VideoScrubber> { +class _VideoScrubberState extends State { bool _controllerWasPlaying = false; VideoPlayerController get controller => widget.controller; @@ -1013,7 +1018,7 @@ class _VideoProgressIndicatorState extends State { child: progressIndicator, ); if (widget.allowScrubbing) { - return _VideoScrubber( + return VideoScrubber( controller: controller, child: paddedProgressIndicator, ); @@ -1095,5 +1100,4 @@ class ClosedCaption extends StatelessWidget { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index a049f0bc770e..d75456ace469 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -3,11 +3,11 @@ description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. repository: https://github.com/flutter/plugins/tree/main/packages/video_player/video_player issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22 -version: 2.4.10 +version: 2.5.1 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index bd71d5fa11e6..56024c4ba233 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.3.10 * Adds compatibilty with version 6.0 of the platform interface. diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle index 2677050d303b..903ee219d881 100644 --- a/packages/video_player/video_player_android/android/build.gradle +++ b/packages/video_player/video_player_android/android/build.gradle @@ -34,9 +34,7 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { - disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -50,7 +48,7 @@ android { implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.18.1' testImplementation 'junit:junit:4.13.2' testImplementation 'androidx.test:core:1.3.0' - testImplementation 'org.mockito:mockito-inline:4.7.0' + testImplementation 'org.mockito:mockito-inline:5.0.0' testImplementation 'org.robolectric:robolectric:4.8.1' } diff --git a/packages/video_player/video_player_android/example/android/app/build.gradle b/packages/video_player/video_player_android/example/android/app/build.gradle index 93caaa5c7c61..80de4a1ee27d 100644 --- a/packages/video_player/video_player_android/example/android/app/build.gradle +++ b/packages/video_player/video_player_android/example/android/app/build.gradle @@ -60,7 +60,7 @@ flutter { dependencies { testImplementation 'junit:junit:4.13' testImplementation 'org.robolectric:robolectric:4.8.2' - testImplementation 'org.mockito:mockito-core:4.7.0' + testImplementation 'org.mockito:mockito-core:5.0.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' } diff --git a/packages/video_player/video_player_android/example/android/gradle.properties b/packages/video_player/video_player_android/example/android/gradle.properties index a6738207fd15..e5611e4c7fa0 100644 --- a/packages/video_player/video_player_android/example/android/gradle.properties +++ b/packages/video_player/video_player_android/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/video_player/video_player_android/example/pubspec.yaml b/packages/video_player/video_player_android/example/pubspec.yaml index 29865ed3cc24..16ffe17e7ba3 100644 --- a/packages/video_player/video_player_android/example/pubspec.yaml +++ b/packages/video_player/video_player_android/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/video_player/video_player_android/pigeons/messages.dart b/packages/video_player/video_player_android/pigeons/messages.dart index bf552f9369df..90c9fbb61ea0 100644 --- a/packages/video_player/video_player_android/pigeons/messages.dart +++ b/packages/video_player/video_player_android/pigeons/messages.dart @@ -6,7 +6,7 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', - dartTestOut: 'test/test_api.dart', + dartTestOut: 'test/test_api.g.dart', javaOut: 'android/src/main/java/io/flutter/plugins/videoplayer/Messages.java', javaOptions: JavaOptions( package: 'io.flutter.plugins.videoplayer', diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index 2e1321f3bb43..3f46ec8a4d79 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.3.10 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/video_player/video_player_android/test/android_video_player_test.dart b/packages/video_player/video_player_android/test/android_video_player_test.dart index fad9617ddad9..6aa24e5c1808 100644 --- a/packages/video_player/video_player_android/test/android_video_player_test.dart +++ b/packages/video_player/video_player_android/test/android_video_player_test.dart @@ -12,7 +12,7 @@ import 'package:video_player_android/src/messages.g.dart'; import 'package:video_player_android/video_player_android.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; -import 'test_api.dart'; +import 'test_api.g.dart'; class _ApiLogger implements TestHostVideoPlayerApi { final List log = []; @@ -234,16 +234,16 @@ void main() { }); test('videoEventsFor', () async { - _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .setMockMessageHandler( 'flutter.io/videoPlayer/videoEvents123', (ByteData? message) async { final MethodCall methodCall = const StandardMethodCodec().decodeMethodCall(message); if (methodCall.method == 'listen') { - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -255,8 +255,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -269,8 +269,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -279,8 +279,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -293,8 +293,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -303,8 +303,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -360,5 +360,4 @@ void main() { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/video_player/video_player_android/test/test_api.dart b/packages/video_player/video_player_android/test/test_api.g.dart similarity index 100% rename from packages/video_player/video_player_android/test/test_api.dart rename to packages/video_player/video_player_android/test/test_api.g.dart diff --git a/packages/video_player/video_player_avfoundation/CHANGELOG.md b/packages/video_player/video_player_avfoundation/CHANGELOG.md index cf9c035abcda..b8564c0a2236 100644 --- a/packages/video_player/video_player_avfoundation/CHANGELOG.md +++ b/packages/video_player/video_player_avfoundation/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.3.8 * Adds compatibilty with version 6.0 of the platform interface. diff --git a/packages/video_player/video_player_avfoundation/example/pubspec.yaml b/packages/video_player/video_player_avfoundation/example/pubspec.yaml index f101b697a9cb..422fb91e35e5 100644 --- a/packages/video_player/video_player_avfoundation/example/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/video_player/video_player_avfoundation/pigeons/messages.dart b/packages/video_player/video_player_avfoundation/pigeons/messages.dart index e6eda5960f29..695ff34e3ebd 100644 --- a/packages/video_player/video_player_avfoundation/pigeons/messages.dart +++ b/packages/video_player/video_player_avfoundation/pigeons/messages.dart @@ -6,7 +6,7 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', - dartTestOut: 'test/test_api.dart', + dartTestOut: 'test/test_api.g.dart', objcHeaderOut: 'ios/Classes/messages.g.h', objcSourceOut: 'ios/Classes/messages.g.m', objcOptions: ObjcOptions( diff --git a/packages/video_player/video_player_avfoundation/pubspec.yaml b/packages/video_player/video_player_avfoundation/pubspec.yaml index 116edde94955..a5204137af20 100644 --- a/packages/video_player/video_player_avfoundation/pubspec.yaml +++ b/packages/video_player/video_player_avfoundation/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.3.8 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart index ea81d438ad75..c01373f05424 100644 --- a/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart +++ b/packages/video_player/video_player_avfoundation/test/avfoundation_video_player_test.dart @@ -12,7 +12,7 @@ import 'package:video_player_avfoundation/src/messages.g.dart'; import 'package:video_player_avfoundation/video_player_avfoundation.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; -import 'test_api.dart'; +import 'test_api.g.dart'; class _ApiLogger implements TestHostVideoPlayerApi { final List log = []; @@ -234,16 +234,16 @@ void main() { }); test('videoEventsFor', () async { - _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .setMockMessageHandler( 'flutter.io/videoPlayer/videoEvents123', (ByteData? message) async { final MethodCall methodCall = const StandardMethodCodec().decodeMethodCall(message); if (methodCall.method == 'listen') { - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -255,8 +255,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -265,8 +265,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -279,8 +279,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -289,8 +289,8 @@ void main() { }), (ByteData? data) {}); - await _ambiguate(ServicesBinding.instance) - ?.defaultBinaryMessenger + await _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( 'flutter.io/videoPlayer/videoEvents123', const StandardMethodCodec() @@ -339,5 +339,4 @@ void main() { /// /// We use this so that APIs that have become non-nullable can still be used /// with `!` and `?` on the stable branch. -// TODO(ianh): Remove this once we roll stable in late 2021. T? _ambiguate(T? value) => value; diff --git a/packages/video_player/video_player_avfoundation/test/test_api.dart b/packages/video_player/video_player_avfoundation/test/test_api.g.dart similarity index 100% rename from packages/video_player/video_player_avfoundation/test/test_api.dart rename to packages/video_player/video_player_avfoundation/test/test_api.g.dart diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index fb7a3b340ca9..e1acbf578027 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 6.0.1 * Fixes comment describing file URI construction. diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 36c0a7181845..8c6a8f400bb2 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 6.0.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index 1f6dc951a6ff..42355439ce12 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.0.13 * Adds compatibilty with version 6.0 of the platform interface. diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml index 1d12b4ffcfd4..c4de1ce54c1a 100644 --- a/packages/video_player/video_player_web/example/pubspec.yaml +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -3,7 +3,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index 0cd1c11f7f38..5e603034dd28 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -6,7 +6,7 @@ version: 2.0.13 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/webview_flutter/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md index 4eb0483a9f3c..84f890790128 100644 --- a/packages/webview_flutter/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -1,3 +1,15 @@ +## 4.0.4 + +* Adds examples of accessing platform-specific features for each class. + +## 4.0.3 + +* Updates example code for `use_build_context_synchronously` lint. + +## 4.0.2 + +* Updates code for stricter lint checks. + ## 4.0.1 * Exposes `WebResourceErrorType` from platform interface. diff --git a/packages/webview_flutter/webview_flutter/README.md b/packages/webview_flutter/webview_flutter/README.md index 98f7b667025b..b30b8bc20fa1 100644 --- a/packages/webview_flutter/webview_flutter/README.md +++ b/packages/webview_flutter/webview_flutter/README.md @@ -99,7 +99,7 @@ import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; ``` Now, additional features can be accessed through the platform implementations. Classes -`WebViewController`, `WebViewWidget`, `NavigationDelegate`, and `WebViewCookieManager` pass their +[WebViewController], [WebViewWidget], [NavigationDelegate], and [WebViewCookieManager] pass their functionality to a class provided by the current platform. Below are a couple of ways to access additional functionality provided by the platform and is followed by an example. @@ -212,11 +212,17 @@ Below is a non-exhaustive list of changes to the API: * `WebView.userAgent` -> `WebViewController.setUserAgent` * `WebView.backgroundColor` -> `WebViewController.setBackgroundColor` * The following features have been moved to an Android implementation class. See section - `Platform-Specific Features` for details on accessing Android platform specific features. + `Platform-Specific Features` for details on accessing Android platform-specific features. * `WebView.debuggingEnabled` -> `static AndroidWebViewController.enableDebugging` * `WebView.initialMediaPlaybackPolicy` -> `AndroidWebViewController.setMediaPlaybackRequiresUserGesture` * The following features have been moved to an iOS implementation class. See section - `Platform-Specific Features` for details on accessing iOS platform specific features. + `Platform-Specific Features` for details on accessing iOS platform-specific features. * `WebView.gestureNavigationEnabled` -> `WebKitWebViewController.setAllowsBackForwardNavigationGestures` * `WebView.initialMediaPlaybackPolicy` -> `WebKitWebViewControllerCreationParams.mediaTypesRequiringUserAction` * `WebView.allowsInlineMediaPlayback` -> `WebKitWebViewControllerCreationParams.allowsInlineMediaPlayback` + + +[WebViewController]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewController-class.html +[WebViewWidget]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewWidget-class.html +[NavigationDelegate]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/NavigationDelegate-class.html +[WebViewCookieManager]: https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewCookieManager-class.html \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter/example/android/gradle.properties b/packages/webview_flutter/webview_flutter/example/android/gradle.properties index a6738207fd15..e5611e4c7fa0 100644 --- a/packages/webview_flutter/webview_flutter/example/android/gradle.properties +++ b/packages/webview_flutter/webview_flutter/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart index 239b417c4e04..ec1ce4eef16c 100644 --- a/packages/webview_flutter/webview_flutter/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -180,9 +180,11 @@ Page resource error: return FloatingActionButton( onPressed: () async { final String? url = await _controller.currentUrl(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Favorited $url')), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + } }, child: const Icon(Icons.favorite), ); @@ -330,25 +332,29 @@ class SampleMenu extends StatelessWidget { Future _onListCookies(BuildContext context) async { final String cookies = await webViewController .runJavaScriptReturningResult('document.cookie') as String; - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Cookies:'), - _getCookieList(cookies), - ], - ), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } } Future _onAddToCache(BuildContext context) async { await webViewController.runJavaScript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";', ); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Added a test entry to cache.'), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } } Future _onListCache() { @@ -361,9 +367,11 @@ class SampleMenu extends StatelessWidget { Future _onClearCache(BuildContext context) async { await webViewController.clearCache(); await webViewController.clearLocalStorage(); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Cache cleared.'), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Cache cleared.'), + )); + } } Future _onClearCookies(BuildContext context) async { @@ -372,9 +380,11 @@ class SampleMenu extends StatelessWidget { if (!hadCookies) { message = 'There are no cookies.'; } - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(message), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } } Future _onNavigationDelegateExample() { @@ -467,10 +477,11 @@ class NavigationControls extends StatelessWidget { if (await webViewController.canGoBack()) { await webViewController.goBack(); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No back history item')), - ); - return; + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No back history item')), + ); + } } }, ), @@ -480,10 +491,11 @@ class NavigationControls extends StatelessWidget { if (await webViewController.canGoForward()) { await webViewController.goForward(); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No forward history item')), - ); - return; + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No forward history item')), + ); + } } }, ), diff --git a/packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart index 8d7baa9fa5af..d210e1e7669a 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/legacy/webview.dart @@ -127,6 +127,7 @@ class WebView extends StatefulWidget { case TargetPlatform.iOS: _platform = CupertinoWebView(); break; + // ignore: no_default_cases default: throw UnsupportedError( "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); diff --git a/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart index 0651ad45f229..3237fa41c0bb 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/navigation_delegate.dart @@ -12,6 +12,28 @@ import 'webview_controller.dart'; /// the progress of navigation requests. /// /// See [WebViewController.setNavigationDelegate]. +/// +/// ## Platform-Specific Features +/// This class contains an underlying implementation provided by the current +/// platform. Once a platform implementation is imported, the examples below +/// can be followed to use features provided by a platform's implementation. +/// +/// {@macro webview_flutter.NavigationDelegate.fromPlatformCreationParams} +/// +/// Below is an example of accessing the platform-specific implementation for +/// iOS and Android: +/// +/// ```dart +/// final NavigationDelegate navigationDelegate = NavigationDelegate(); +/// +/// if (WebViewPlatform.instance is WebKitWebViewPlatform) { +/// final WebKitNavigationDelegate webKitDelegate = +/// navigationDelegate.platform as WebKitNavigationDelegate; +/// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { +/// final AndroidNavigationDelegate androidDelegate = +/// navigationDelegate.platform as AndroidNavigationDelegate; +/// } +/// ``` class NavigationDelegate { /// Constructs a [NavigationDelegate]. NavigationDelegate({ @@ -32,6 +54,33 @@ class NavigationDelegate { /// Constructs a [NavigationDelegate] from creation params for a specific /// platform. + /// + /// {@template webview_flutter.NavigationDelegate.fromPlatformCreationParams} + /// Below is an example of setting platform-specific creation parameters for + /// iOS and Android: + /// + /// ```dart + /// PlatformNavigationDelegateCreationParams params = + /// const PlatformNavigationDelegateCreationParams(); + /// + /// if (WebViewPlatform.instance is WebKitWebViewPlatform) { + /// params = WebKitNavigationDelegateCreationParams + /// .fromPlatformNavigationDelegateCreationParams( + /// params, + /// ); + /// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { + /// params = AndroidNavigationDelegateCreationParams + /// .fromPlatformNavigationDelegateCreationParams( + /// params, + /// ); + /// } + /// + /// final NavigationDelegate navigationDelegate = + /// NavigationDelegate.fromPlatformCreationParams( + /// params, + /// ); + /// ``` + /// {@endtemplate} NavigationDelegate.fromPlatformCreationParams( PlatformNavigationDelegateCreationParams params, { FutureOr Function(NavigationRequest request)? diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart index d632d1e95231..a112f1522579 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview_controller.dart @@ -10,12 +10,41 @@ import 'package:flutter/material.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'navigation_delegate.dart'; +import 'webview_widget.dart'; /// Controls a WebView provided by the host platform. /// /// Pass this to a [WebViewWidget] to display the WebView. +/// +/// A [WebViewController] can only be used by a single [WebViewWidget] at a +/// time. +/// +/// ## Platform-Specific Features +/// This class contains an underlying implementation provided by the current +/// platform. Once a platform implementation is imported, the examples below +/// can be followed to use features provided by a platform's implementation. +/// +/// {@macro webview_flutter.WebViewController.fromPlatformCreationParams} +/// +/// Below is an example of accessing the platform-specific implementation for +/// iOS and Android: +/// +/// ```dart +/// final WebViewController webViewController = WebViewController(); +/// +/// if (WebViewPlatform.instance is WebKitWebViewPlatform) { +/// final WebKitWebViewController webKitController = +/// webViewController.platform as WebKitWebViewController; +/// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { +/// final AndroidWebViewController androidController = +/// webViewController.platform as AndroidWebViewController; +/// } +/// ``` class WebViewController { /// Constructs a [WebViewController]. + /// + /// See [WebViewController.fromPlatformCreationParams] for setting parameters + /// for a specific platform. WebViewController() : this.fromPlatformCreationParams( const PlatformWebViewControllerCreationParams(), @@ -23,6 +52,33 @@ class WebViewController { /// Constructs a [WebViewController] from creation params for a specific /// platform. + /// + /// {@template webview_flutter.WebViewCookieManager.fromPlatformCreationParams} + /// Below is an example of setting platform-specific creation parameters for + /// iOS and Android: + /// + /// ```dart + /// PlatformWebViewControllerCreationParams params = + /// const PlatformWebViewControllerCreationParams(); + /// + /// if (WebViewPlatform.instance is WebKitWebViewPlatform) { + /// params = WebKitWebViewControllerCreationParams + /// .fromPlatformWebViewControllerCreationParams( + /// params, + /// ); + /// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { + /// params = AndroidWebViewControllerCreationParams + /// .fromPlatformWebViewControllerCreationParams( + /// params, + /// ); + /// } + /// + /// final WebViewController webViewController = + /// WebViewController.fromPlatformCreationParams( + /// params, + /// ); + /// ``` + /// {@endtemplate} WebViewController.fromPlatformCreationParams( PlatformWebViewControllerCreationParams params, ) : this.fromPlatform(PlatformWebViewController(params)); diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_cookie_manager.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_cookie_manager.dart index bffa1b5a71d2..353d7554fcb2 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview_cookie_manager.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview_cookie_manager.dart @@ -5,8 +5,33 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; /// Manages cookies pertaining to all WebViews. +/// +/// ## Platform-Specific Features +/// This class contains an underlying implementation provided by the current +/// platform. Once a platform implementation is imported, the examples below +/// can be followed to use features provided by a platform's implementation. +/// +/// {@macro webview_flutter.WebViewCookieManager.fromPlatformCreationParams} +/// +/// Below is an example of accessing the platform-specific implementation for +/// iOS and Android: +/// +/// ```dart +/// final WebViewCookieManager cookieManager = WebViewCookieManager(); +/// +/// if (WebViewPlatform.instance is WebKitWebViewPlatform) { +/// final WebKitWebViewCookieManager webKitManager = +/// cookieManager.platform as WebKitWebViewCookieManager; +/// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { +/// final AndroidWebViewCookieManager androidManager = +/// cookieManager.platform as AndroidWebViewCookieManager; +/// } +/// ``` class WebViewCookieManager { /// Constructs a [WebViewCookieManager]. + /// + /// See [WebViewCookieManager.fromPlatformCreationParams] for setting + /// parameters for a specific platform. WebViewCookieManager() : this.fromPlatformCreationParams( const PlatformWebViewCookieManagerCreationParams(), @@ -14,6 +39,33 @@ class WebViewCookieManager { /// Constructs a [WebViewCookieManager] from creation params for a specific /// platform. + /// + /// {@template webview_flutter.WebViewCookieManager.fromPlatformCreationParams} + /// Below is an example of setting platform-specific creation parameters for + /// iOS and Android: + /// + /// ```dart + /// PlatformWebViewCookieManagerCreationParams params = + /// const PlatformWebViewCookieManagerCreationParams(); + /// + /// if (WebViewPlatform.instance is WebKitWebViewPlatform) { + /// params = WebKitWebViewCookieManagerCreationParams + /// .fromPlatformWebViewCookieManagerCreationParams( + /// params, + /// ); + /// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { + /// params = AndroidWebViewCookieManagerCreationParams + /// .fromPlatformWebViewCookieManagerCreationParams( + /// params, + /// ); + /// } + /// + /// final WebViewCookieManager webViewCookieManager = + /// WebViewCookieManager.fromPlatformCreationParams( + /// params, + /// ); + /// ``` + /// {@endtemplate} WebViewCookieManager.fromPlatformCreationParams( PlatformWebViewCookieManagerCreationParams params, ) : this.fromPlatform(PlatformWebViewCookieManager(params)); diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview_widget.dart b/packages/webview_flutter/webview_flutter/lib/src/webview_widget.dart index b3180115c801..440d0f6654ec 100644 --- a/packages/webview_flutter/webview_flutter/lib/src/webview_widget.dart +++ b/packages/webview_flutter/webview_flutter/lib/src/webview_widget.dart @@ -10,8 +10,35 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_inte import 'webview_controller.dart'; /// Displays a native WebView as a Widget. +/// +/// ## Platform-Specific Features +/// This class contains an underlying implementation provided by the current +/// platform. Once a platform implementation is imported, the examples below +/// can be followed to use features provided by a platform's implementation. +/// +/// {@macro webview_flutter.WebViewWidget.fromPlatformCreationParams} +/// +/// Below is an example of accessing the platform-specific implementation for +/// iOS and Android: +/// +/// ```dart +/// final WebViewController controller = WebViewController(); +/// +/// final WebViewWidget webViewWidget = WebViewWidget(controller: controller); +/// +/// if (WebViewPlatform.instance is WebKitWebViewPlatform) { +/// final WebKitWebViewWidget webKitWidget = +/// webViewWidget.platform as WebKitWebViewWidget; +/// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { +/// final AndroidWebViewWidget androidWidget = +/// webViewWidget.platform as AndroidWebViewWidget; +/// } +/// ``` class WebViewWidget extends StatelessWidget { /// Constructs a [WebViewWidget]. + /// + /// See [WebViewWidget.fromPlatformCreationParams] for setting parameters for + /// a specific platform. WebViewWidget({ Key? key, required WebViewController controller, @@ -27,8 +54,40 @@ class WebViewWidget extends StatelessWidget { ), ); - /// Constructs a [WebViewWidget] from creation params for a specific - /// platform. + /// Constructs a [WebViewWidget] from creation params for a specific platform. + /// + /// {@template webview_flutter.WebViewWidget.fromPlatformCreationParams} + /// Below is an example of setting platform-specific creation parameters for + /// iOS and Android: + /// + /// ```dart + /// final WebViewController controller = WebViewController(); + /// + /// PlatformWebViewWidgetCreationParams params = + /// PlatformWebViewWidgetCreationParams( + /// controller: controller.platform, + /// layoutDirection: TextDirection.ltr, + /// gestureRecognizers: const >{}, + /// ); + /// + /// if (WebViewPlatform.instance is WebKitWebViewPlatform) { + /// params = WebKitWebViewWidgetCreationParams + /// .fromPlatformWebViewWidgetCreationParams( + /// params, + /// ); + /// } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { + /// params = AndroidWebViewWidgetCreationParams + /// .fromPlatformWebViewWidgetCreationParams( + /// params, + /// ); + /// } + /// + /// final WebViewWidget webViewWidget = + /// WebViewWidget.fromPlatformCreationParams( + /// params: params, + /// ); + /// ``` + /// {@endtemplate} WebViewWidget.fromPlatformCreationParams({ Key? key, required PlatformWebViewWidgetCreationParams params, diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml index ace6207d6236..5cef1a731739 100644 --- a/packages/webview_flutter/webview_flutter/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter description: A Flutter plugin that provides a WebView widget on Android and iOS. repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 4.0.1 +version: 4.0.4 environment: sdk: ">=2.17.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index 37abf3cf2b1b..ed6c546ed147 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,29 @@ +## 3.3.0 + +* Adds support to access native `WebView`. + +## 3.2.4 + +* Renames Pigeon output files. + +## 3.2.3 + +* Fixes bug that prevented the web view from being garbage collected. +* Fixes bug causing a `LateInitializationError` when a `PlatformNavigationDelegate` is not provided. + +## 3.2.2 + +* Updates example code for `use_build_context_synchronously` lint. + +## 3.2.1 + +* Updates code for stricter lint checks. + +## 3.2.0 + +* Adds support for handling file selection. See `AndroidWebViewController.setOnShowFileSelector`. +* Updates pigeon dev dependency to `4.2.14`. + ## 3.1.3 * Fixes crash when the Java `InstanceManager` was used after plugin was removed from the engine. diff --git a/packages/webview_flutter/webview_flutter_android/README.md b/packages/webview_flutter/webview_flutter_android/README.md index 1a54808379fb..d2f4d94bfed4 100644 --- a/packages/webview_flutter/webview_flutter_android/README.md +++ b/packages/webview_flutter/webview_flutter_android/README.md @@ -32,6 +32,22 @@ This can be configured for versions >=23 with `AndroidWebViewWidgetCreationParams.displayWithHybridComposition`. See https://pub.dev/packages/webview_flutter#platform-specific-features for more details on setting platform-specific features in the main plugin. +### External Native API + +The plugin also provides a native API accessible by the native code of Android applications or +packages. This API follows the convention of breaking changes of the Dart API, which means that any +changes to the class that are not backwards compatible will only be made with a major version change +of the plugin. Native code other than this external API does not follow breaking change conventions, +so app or plugin clients should not use any other native APIs. + +The API can be accessed by importing the native class `WebViewFlutterAndroidExternalApi`: + +Java: + +```java +import io.flutter.plugins.webviewflutter.WebViewFlutterAndroidExternalApi; +``` + ## Contributing This package uses [pigeon][3] to generate the communication layer between Flutter and the host diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle index 7384f8d453da..6783e1c977c2 100644 --- a/packages/webview_flutter/webview_flutter_android/android/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle @@ -30,16 +30,14 @@ android { } lintOptions { - disable 'AndroidGradlePluginVersion' - disable 'InvalidPackage' - disable 'GradleDependency' + disable 'AndroidGradlePluginVersion', 'InvalidPackage', 'GradleDependency' } dependencies { implementation 'androidx.annotation:annotation:1.5.0' implementation 'androidx.webkit:webkit:1.5.0' testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-inline:4.8.0' + testImplementation 'org.mockito:mockito-inline:5.1.0' testImplementation 'androidx.test:core:1.3.0' } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FileChooserParamsFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FileChooserParamsFlutterApiImpl.java new file mode 100644 index 000000000000..679785949697 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FileChooserParamsFlutterApiImpl.java @@ -0,0 +1,74 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.os.Build; +import android.webkit.WebChromeClient; +import androidx.annotation.RequiresApi; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Arrays; + +/** + * Flutter Api implementation for {@link android.webkit.WebChromeClient.FileChooserParams}. + * + *

Passes arguments of callbacks methods from a {@link + * android.webkit.WebChromeClient.FileChooserParams} to Dart. + */ +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class FileChooserParamsFlutterApiImpl + extends GeneratedAndroidWebView.FileChooserParamsFlutterApi { + private final InstanceManager instanceManager; + + /** + * Creates a Flutter api that sends messages to Dart. + * + * @param binaryMessenger handles sending messages to Dart + * @param instanceManager maintains instances stored to communicate with Dart objects + */ + public FileChooserParamsFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + private static GeneratedAndroidWebView.FileChooserModeEnumData toFileChooserEnumData(int mode) { + final GeneratedAndroidWebView.FileChooserModeEnumData.Builder builder = + new GeneratedAndroidWebView.FileChooserModeEnumData.Builder(); + + switch (mode) { + case WebChromeClient.FileChooserParams.MODE_OPEN: + builder.setValue(GeneratedAndroidWebView.FileChooserMode.OPEN); + break; + case WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE: + builder.setValue(GeneratedAndroidWebView.FileChooserMode.OPEN_MULTIPLE); + break; + case WebChromeClient.FileChooserParams.MODE_SAVE: + builder.setValue(GeneratedAndroidWebView.FileChooserMode.SAVE); + break; + default: + throw new IllegalArgumentException(String.format("Unsupported FileChooserMode: %d", mode)); + } + + return builder.build(); + } + + /** + * Stores the FileChooserParams instance and notifies Dart to create a new FileChooserParams + * instance that is attached to this one. + * + * @return the instanceId of the stored instance + */ + public long create(WebChromeClient.FileChooserParams instance, Reply callback) { + final long instanceId = instanceManager.addHostCreatedInstance(instance); + create( + instanceId, + instance.isCaptureEnabled(), + Arrays.asList(instance.getAcceptTypes()), + toFileChooserEnumData(instance.getMode()), + instance.getFilenameHint(), + callback); + return instanceId; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java index 15c80cc0a907..425f6c1415bd 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v4.2.3), do not edit directly. +// Autogenerated from Pigeon (v4.2.14), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.webviewflutter; @@ -18,7 +18,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -26,6 +25,90 @@ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) public class GeneratedAndroidWebView { + /** + * Mode of how to select files for a file chooser. + * + *

See + * https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. + */ + public enum FileChooserMode { + /** + * Open single file and requires that the file exists before allowing the user to pick it. + * + *

See + * https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_OPEN. + */ + OPEN(0), + /** + * Similar to [open] but allows multiple files to be selected. + * + *

See + * https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_OPEN_MULTIPLE. + */ + OPEN_MULTIPLE(1), + /** + * Allows picking a nonexistent file and saving it. + * + *

See + * https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_SAVE. + */ + SAVE(2); + + private final int index; + + private FileChooserMode(final int index) { + this.index = index; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class FileChooserModeEnumData { + private @NonNull FileChooserMode value; + + public @NonNull FileChooserMode getValue() { + return value; + } + + public void setValue(@NonNull FileChooserMode setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"value\" is null."); + } + this.value = setterArg; + } + + /** Constructor is private to enforce null safety; use Builder. */ + private FileChooserModeEnumData() {} + + public static final class Builder { + private @Nullable FileChooserMode value; + + public @NonNull Builder setValue(@NonNull FileChooserMode setterArg) { + this.value = setterArg; + return this; + } + + public @NonNull FileChooserModeEnumData build() { + FileChooserModeEnumData pigeonReturn = new FileChooserModeEnumData(); + pigeonReturn.setValue(value); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(1); + toListResult.add(value == null ? null : value.index); + return toListResult; + } + + static @NonNull FileChooserModeEnumData fromList(@NonNull ArrayList list) { + FileChooserModeEnumData pigeonResult = new FileChooserModeEnumData(); + Object value = list.get(0); + pigeonResult.setValue(value == null ? null : FileChooserMode.values()[(int) value]); + return pigeonResult; + } + } + /** Generated class from Pigeon that represents data sent in messages. */ public static class WebResourceRequestData { private @NonNull String url; @@ -162,30 +245,30 @@ public static final class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("url", url); - toMapResult.put("isForMainFrame", isForMainFrame); - toMapResult.put("isRedirect", isRedirect); - toMapResult.put("hasGesture", hasGesture); - toMapResult.put("method", method); - toMapResult.put("requestHeaders", requestHeaders); - return toMapResult; - } - - static @NonNull WebResourceRequestData fromMap(@NonNull Map map) { + ArrayList toList() { + ArrayList toListResult = new ArrayList(6); + toListResult.add(url); + toListResult.add(isForMainFrame); + toListResult.add(isRedirect); + toListResult.add(hasGesture); + toListResult.add(method); + toListResult.add(requestHeaders); + return toListResult; + } + + static @NonNull WebResourceRequestData fromList(@NonNull ArrayList list) { WebResourceRequestData pigeonResult = new WebResourceRequestData(); - Object url = map.get("url"); + Object url = list.get(0); pigeonResult.setUrl((String) url); - Object isForMainFrame = map.get("isForMainFrame"); + Object isForMainFrame = list.get(1); pigeonResult.setIsForMainFrame((Boolean) isForMainFrame); - Object isRedirect = map.get("isRedirect"); + Object isRedirect = list.get(2); pigeonResult.setIsRedirect((Boolean) isRedirect); - Object hasGesture = map.get("hasGesture"); + Object hasGesture = list.get(3); pigeonResult.setHasGesture((Boolean) hasGesture); - Object method = map.get("method"); + Object method = list.get(4); pigeonResult.setMethod((String) method); - Object requestHeaders = map.get("requestHeaders"); + Object requestHeaders = list.get(5); pigeonResult.setRequestHeaders((Map) requestHeaders); return pigeonResult; } @@ -246,21 +329,21 @@ public static final class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("errorCode", errorCode); - toMapResult.put("description", description); - return toMapResult; + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(errorCode); + toListResult.add(description); + return toListResult; } - static @NonNull WebResourceErrorData fromMap(@NonNull Map map) { + static @NonNull WebResourceErrorData fromList(@NonNull ArrayList list) { WebResourceErrorData pigeonResult = new WebResourceErrorData(); - Object errorCode = map.get("errorCode"); + Object errorCode = list.get(0); pigeonResult.setErrorCode( (errorCode == null) ? null : ((errorCode instanceof Integer) ? (Integer) errorCode : (Long) errorCode)); - Object description = map.get("description"); + Object description = list.get(1); pigeonResult.setDescription((String) description); return pigeonResult; } @@ -321,18 +404,18 @@ public static final class Builder { } @NonNull - Map toMap() { - Map toMapResult = new HashMap<>(); - toMapResult.put("x", x); - toMapResult.put("y", y); - return toMapResult; + ArrayList toList() { + ArrayList toListResult = new ArrayList(2); + toListResult.add(x); + toListResult.add(y); + return toListResult; } - static @NonNull WebViewPoint fromMap(@NonNull Map map) { + static @NonNull WebViewPoint fromList(@NonNull ArrayList list) { WebViewPoint pigeonResult = new WebViewPoint(); - Object x = map.get("x"); + Object x = list.get(0); pigeonResult.setX((x == null) ? null : ((x instanceof Integer) ? (Integer) x : (Long) x)); - Object y = map.get("y"); + Object y = list.get(1); pigeonResult.setY((y == null) ? null : ((y instanceof Integer) ? (Integer) y : (Long) y)); return pigeonResult; } @@ -370,7 +453,7 @@ static void setup(BinaryMessenger binaryMessenger, JavaObjectHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -379,9 +462,10 @@ static void setup(BinaryMessenger binaryMessenger, JavaObjectHostApi api) { throw new NullPointerException("identifierArg unexpectedly null."); } api.dispose((identifierArg == null) ? null : identifierArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -448,25 +532,25 @@ static void setup(BinaryMessenger binaryMessenger, CookieManagerHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { Result resultCallback = new Result() { public void success(Boolean result) { - wrapped.put("result", result); + wrapped.add(0, result); reply.reply(wrapped); } public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); } }; api.clearCookies(resultCallback); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); + ArrayList wrappedError = wrapError(exception); + reply.reply(wrappedError); } }); } else { @@ -480,7 +564,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -493,9 +577,10 @@ public void error(Throwable error) { throw new NullPointerException("valueArg unexpectedly null."); } api.setCookie(urlArg, valueArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -515,7 +600,7 @@ private WebViewHostApiCodec() {} protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { case (byte) 128: - return WebViewPoint.fromMap((Map) readValue(buffer)); + return WebViewPoint.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -526,7 +611,7 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof WebViewPoint) { stream.write(128); - writeValue(stream, ((WebViewPoint) value).toMap()); + writeValue(stream, ((WebViewPoint) value).toList()); } else { super.writeValue(stream, value); } @@ -620,7 +705,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -635,9 +720,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { api.create( (instanceIdArg == null) ? null : instanceIdArg.longValue(), useHybridCompositionArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -652,7 +738,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -671,9 +757,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { dataArg, mimeTypeArg, encodingArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -690,7 +777,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -713,9 +800,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { mimeTypeArg, encodingArg, historyUrlArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -730,7 +818,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -750,9 +838,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { (instanceIdArg == null) ? null : instanceIdArg.longValue(), urlArg, headersArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -767,7 +856,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -785,9 +874,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { } api.postUrl( (instanceIdArg == null) ? null : instanceIdArg.longValue(), urlArg, dataArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -802,7 +892,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -812,9 +902,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { } String output = api.getUrl((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -829,7 +920,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -839,9 +930,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { } Boolean output = api.canGoBack((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -856,7 +948,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -866,9 +958,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { } Boolean output = api.canGoForward((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -883,7 +976,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -892,9 +985,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.goBack((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -909,7 +1003,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -918,9 +1012,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.goForward((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -935,7 +1030,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -944,9 +1039,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.reload((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -961,7 +1057,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -976,9 +1072,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { api.clearCache( (instanceIdArg == null) ? null : instanceIdArg.longValue(), includeDiskFilesArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -995,7 +1092,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1010,13 +1107,13 @@ static void setup(BinaryMessenger binaryMessenger, WebViewHostApi api) { Result resultCallback = new Result() { public void success(String result) { - wrapped.put("result", result); + wrapped.add(0, result); reply.reply(wrapped); } public void error(Throwable error) { - wrapped.put("error", wrapError(error)); - reply.reply(wrapped); + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); } }; @@ -1025,8 +1122,8 @@ public void error(Throwable error) { javascriptStringArg, resultCallback); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); - reply.reply(wrapped); + ArrayList wrappedError = wrapError(exception); + reply.reply(wrappedError); } }); } else { @@ -1040,7 +1137,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1050,9 +1147,10 @@ public void error(Throwable error) { } String output = api.getTitle((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1067,7 +1165,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1087,9 +1185,10 @@ public void error(Throwable error) { (instanceIdArg == null) ? null : instanceIdArg.longValue(), (xArg == null) ? null : xArg.longValue(), (yArg == null) ? null : yArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1104,7 +1203,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1124,9 +1223,10 @@ public void error(Throwable error) { (instanceIdArg == null) ? null : instanceIdArg.longValue(), (xArg == null) ? null : xArg.longValue(), (yArg == null) ? null : yArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1141,7 +1241,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1151,9 +1251,10 @@ public void error(Throwable error) { } Long output = api.getScrollX((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1168,7 +1269,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1178,9 +1279,10 @@ public void error(Throwable error) { } Long output = api.getScrollY((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1195,7 +1297,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1206,9 +1308,10 @@ public void error(Throwable error) { WebViewPoint output = api.getScrollPosition( (instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1225,7 +1328,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1234,9 +1337,10 @@ public void error(Throwable error) { throw new NullPointerException("enabledArg unexpectedly null."); } api.setWebContentsDebuggingEnabled(enabledArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1251,7 +1355,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1268,9 +1372,10 @@ public void error(Throwable error) { (webViewClientInstanceIdArg == null) ? null : webViewClientInstanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1287,7 +1392,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1305,9 +1410,10 @@ public void error(Throwable error) { (javaScriptChannelInstanceIdArg == null) ? null : javaScriptChannelInstanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1324,7 +1430,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1342,9 +1448,10 @@ public void error(Throwable error) { (javaScriptChannelInstanceIdArg == null) ? null : javaScriptChannelInstanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1361,7 +1468,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1373,9 +1480,10 @@ public void error(Throwable error) { api.setDownloadListener( (instanceIdArg == null) ? null : instanceIdArg.longValue(), (listenerInstanceIdArg == null) ? null : listenerInstanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1392,7 +1500,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1404,9 +1512,10 @@ public void error(Throwable error) { api.setWebChromeClient( (instanceIdArg == null) ? null : instanceIdArg.longValue(), (clientInstanceIdArg == null) ? null : clientInstanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1423,7 +1532,7 @@ public void error(Throwable error) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1438,9 +1547,10 @@ public void error(Throwable error) { api.setBackgroundColor( (instanceIdArg == null) ? null : instanceIdArg.longValue(), (colorArg == null) ? null : colorArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1493,7 +1603,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1508,9 +1618,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { api.create( (instanceIdArg == null) ? null : instanceIdArg.longValue(), (webViewInstanceIdArg == null) ? null : webViewInstanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1527,7 +1638,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1541,9 +1652,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setDomStorageEnabled( (instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1560,7 +1672,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1574,9 +1686,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setJavaScriptCanOpenWindowsAutomatically( (instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1593,7 +1706,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1607,9 +1720,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setSupportMultipleWindows( (instanceIdArg == null) ? null : instanceIdArg.longValue(), supportArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1626,7 +1740,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1640,9 +1754,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setJavaScriptEnabled( (instanceIdArg == null) ? null : instanceIdArg.longValue(), flagArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1659,7 +1774,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1671,9 +1786,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { api.setUserAgentString( (instanceIdArg == null) ? null : instanceIdArg.longValue(), userAgentStringArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1690,7 +1806,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1704,9 +1820,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setMediaPlaybackRequiresUserGesture( (instanceIdArg == null) ? null : instanceIdArg.longValue(), requireArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1723,7 +1840,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1737,9 +1854,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setSupportZoom( (instanceIdArg == null) ? null : instanceIdArg.longValue(), supportArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1756,7 +1874,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1770,9 +1888,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setLoadWithOverviewMode( (instanceIdArg == null) ? null : instanceIdArg.longValue(), overviewArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1789,7 +1908,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1803,9 +1922,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setUseWideViewPort( (instanceIdArg == null) ? null : instanceIdArg.longValue(), useArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1822,7 +1942,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1836,9 +1956,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setDisplayZoomControls( (instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1855,7 +1976,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1869,9 +1990,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setBuiltInZoomControls( (instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1888,7 +2010,7 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1902,9 +2024,10 @@ static void setup(BinaryMessenger binaryMessenger, WebSettingsHostApi api) { } api.setAllowFileAccess( (instanceIdArg == null) ? null : instanceIdArg.longValue(), enabledArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -1934,7 +2057,7 @@ static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -1948,9 +2071,10 @@ static void setup(BinaryMessenger binaryMessenger, JavaScriptChannelHostApi api) } api.create( (instanceIdArg == null) ? null : instanceIdArg.longValue(), channelNameArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2013,7 +2137,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -2022,9 +2146,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.create((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2041,7 +2166,7 @@ static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -2055,9 +2180,10 @@ static void setup(BinaryMessenger binaryMessenger, WebViewClientHostApi api) { } api.setSynchronousReturnValueForShouldOverrideUrlLoading( (instanceIdArg == null) ? null : instanceIdArg.longValue(), valueArg); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2077,10 +2203,10 @@ private WebViewClientFlutterApiCodec() {} protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { switch (type) { case (byte) 128: - return WebResourceErrorData.fromMap((Map) readValue(buffer)); + return WebResourceErrorData.fromList((ArrayList) readValue(buffer)); case (byte) 129: - return WebResourceRequestData.fromMap((Map) readValue(buffer)); + return WebResourceRequestData.fromList((ArrayList) readValue(buffer)); default: return super.readValueOfType(type, buffer); @@ -2091,10 +2217,10 @@ protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { if (value instanceof WebResourceErrorData) { stream.write(128); - writeValue(stream, ((WebResourceErrorData) value).toMap()); + writeValue(stream, ((WebResourceErrorData) value).toList()); } else if (value instanceof WebResourceRequestData) { stream.write(129); - writeValue(stream, ((WebResourceRequestData) value).toMap()); + writeValue(stream, ((WebResourceRequestData) value).toList()); } else { super.writeValue(stream, value); } @@ -2247,7 +2373,7 @@ static void setup(BinaryMessenger binaryMessenger, DownloadListenerHostApi api) if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -2256,9 +2382,10 @@ static void setup(BinaryMessenger binaryMessenger, DownloadListenerHostApi api) throw new NullPointerException("instanceIdArg unexpectedly null."); } api.create((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2315,6 +2442,9 @@ public void onDownloadStart( public interface WebChromeClientHostApi { void create(@NonNull Long instanceId); + void setSynchronousReturnValueForOnShowFileChooser( + @NonNull Long instanceId, @NonNull Boolean value); + /** The codec used by WebChromeClientHostApi. */ static MessageCodec getCodec() { return new StandardMessageCodec(); @@ -2331,7 +2461,7 @@ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -2340,9 +2470,44 @@ static void setup(BinaryMessenger binaryMessenger, WebChromeClientHostApi api) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.create((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); + } catch (Error | RuntimeException exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebChromeClientHostApi.setSynchronousReturnValueForOnShowFileChooser", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList<>(); + try { + ArrayList args = (ArrayList) message; + assert args != null; + Number instanceIdArg = (Number) args.get(0); + if (instanceIdArg == null) { + throw new NullPointerException("instanceIdArg unexpectedly null."); + } + Boolean valueArg = (Boolean) args.get(1); + if (valueArg == null) { + throw new NullPointerException("valueArg unexpectedly null."); + } + api.setSynchronousReturnValueForOnShowFileChooser( + (instanceIdArg == null) ? null : instanceIdArg.longValue(), valueArg); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2376,7 +2541,7 @@ static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi ap if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -2385,9 +2550,10 @@ static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi ap throw new NullPointerException("pathArg unexpectedly null."); } List output = api.list(pathArg); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2404,7 +2570,7 @@ static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi ap if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -2413,9 +2579,10 @@ static void setup(BinaryMessenger binaryMessenger, FlutterAssetManagerHostApi ap throw new NullPointerException("nameArg unexpectedly null."); } String output = api.getAssetFilePathByName(nameArg); - wrapped.put("result", output); + wrapped.add(0, output); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2457,6 +2624,26 @@ public void onProgressChanged( callback.reply(null); }); } + + public void onShowFileChooser( + @NonNull Long instanceIdArg, + @NonNull Long webViewInstanceIdArg, + @NonNull Long paramsInstanceIdArg, + Reply> callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.WebChromeClientFlutterApi.onShowFileChooser", + getCodec()); + channel.send( + new ArrayList( + Arrays.asList(instanceIdArg, webViewInstanceIdArg, paramsInstanceIdArg)), + channelReply -> { + @SuppressWarnings("ConstantConditions") + List output = (List) channelReply; + callback.reply(output); + }); + } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface WebStorageHostApi { @@ -2479,7 +2666,7 @@ static void setup(BinaryMessenger binaryMessenger, WebStorageHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -2488,9 +2675,10 @@ static void setup(BinaryMessenger binaryMessenger, WebStorageHostApi api) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.create((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2505,7 +2693,7 @@ static void setup(BinaryMessenger binaryMessenger, WebStorageHostApi api) { if (api != null) { channel.setMessageHandler( (message, reply) -> { - Map wrapped = new HashMap<>(); + ArrayList wrapped = new ArrayList<>(); try { ArrayList args = (ArrayList) message; assert args != null; @@ -2514,9 +2702,10 @@ static void setup(BinaryMessenger binaryMessenger, WebStorageHostApi api) { throw new NullPointerException("instanceIdArg unexpectedly null."); } api.deleteAllData((instanceIdArg == null) ? null : instanceIdArg.longValue()); - wrapped.put("result", null); + wrapped.add(0, null); } catch (Error | RuntimeException exception) { - wrapped.put("error", wrapError(exception)); + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; } reply.reply(wrapped); }); @@ -2527,14 +2716,84 @@ static void setup(BinaryMessenger binaryMessenger, WebStorageHostApi api) { } } + private static class FileChooserParamsFlutterApiCodec extends StandardMessageCodec { + public static final FileChooserParamsFlutterApiCodec INSTANCE = + new FileChooserParamsFlutterApiCodec(); + + private FileChooserParamsFlutterApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return FileChooserModeEnumData.fromList((ArrayList) readValue(buffer)); + + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof FileChooserModeEnumData) { + stream.write(128); + writeValue(stream, ((FileChooserModeEnumData) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** + * Handles callbacks methods for the native Java FileChooserParams class. + * + *

See + * https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. + * + *

Generated class from Pigeon that represents Flutter messages that can be called from Java. + */ + public static class FileChooserParamsFlutterApi { + private final BinaryMessenger binaryMessenger; + + public FileChooserParamsFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + /** The codec used by FileChooserParamsFlutterApi. */ + static MessageCodec getCodec() { + return FileChooserParamsFlutterApiCodec.INSTANCE; + } + + public void create( + @NonNull Long instanceIdArg, + @NonNull Boolean isCaptureEnabledArg, + @NonNull List acceptTypesArg, + @NonNull FileChooserModeEnumData modeArg, + @Nullable String filenameHintArg, + Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.FileChooserParamsFlutterApi.create", getCodec()); + channel.send( + new ArrayList( + Arrays.asList( + instanceIdArg, isCaptureEnabledArg, acceptTypesArg, modeArg, filenameHintArg)), + channelReply -> { + callback.reply(null); + }); + } + } + @NonNull - private static Map wrapError(@NonNull Throwable exception) { - Map errorMap = new HashMap<>(); - errorMap.put("message", exception.toString()); - errorMap.put("code", exception.getClass().getSimpleName()); - errorMap.put( - "details", + private static ArrayList wrapError(@NonNull Throwable exception) { + ArrayList errorList = new ArrayList<>(3); + errorList.add(exception.toString()); + errorList.add(exception.getClass().getSimpleName()); + errorList.add( "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); - return errorMap; + return errorList; } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java index cf263e2a6d34..92f0e41905cc 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientFlutterApiImpl.java @@ -4,10 +4,14 @@ package io.flutter.plugins.webviewflutter; +import android.os.Build; import android.webkit.WebChromeClient; import android.webkit.WebView; +import androidx.annotation.RequiresApi; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientFlutterApi; +import java.util.List; +import java.util.Objects; /** * Flutter Api implementation for {@link WebChromeClient}. @@ -15,6 +19,7 @@ *

Passes arguments of callbacks methods from a {@link WebChromeClient} to Dart. */ public class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { + private final BinaryMessenger binaryMessenger; private final InstanceManager instanceManager; /** @@ -26,6 +31,7 @@ public class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { public WebChromeClientFlutterApiImpl( BinaryMessenger binaryMessenger, InstanceManager instanceManager) { super(binaryMessenger); + this.binaryMessenger = binaryMessenger; this.instanceManager = instanceManager; } @@ -40,6 +46,27 @@ public void onProgressChanged( getIdentifierForClient(webChromeClient), webViewIdentifier, progress, callback); } + /** Passes arguments from {@link WebChromeClient#onShowFileChooser} to Dart. */ + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void onShowFileChooser( + WebChromeClient webChromeClient, + WebView webView, + WebChromeClient.FileChooserParams fileChooserParams, + Reply> callback) { + Long paramsInstanceId = instanceManager.getIdentifierForStrongReference(fileChooserParams); + if (paramsInstanceId == null) { + final FileChooserParamsFlutterApiImpl flutterApi = + new FileChooserParamsFlutterApiImpl(binaryMessenger, instanceManager); + paramsInstanceId = flutterApi.create(fileChooserParams, reply -> {}); + } + + onShowFileChooser( + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(webChromeClient)), + Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(webView)), + paramsInstanceId, + callback); + } + private long getIdentifierForClient(WebChromeClient webChromeClient) { final Long identifier = instanceManager.getIdentifierForStrongReference(webChromeClient); if (identifier == null) { diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java index 3fa4a2f9c298..a5825c0133ec 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebChromeClientHostApiImpl.java @@ -4,8 +4,10 @@ package io.flutter.plugins.webviewflutter; +import android.net.Uri; import android.os.Build; import android.os.Message; +import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebView; @@ -15,6 +17,7 @@ import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi; +import java.util.Objects; /** * Host api implementation for {@link WebChromeClient}. @@ -31,6 +34,7 @@ public class WebChromeClientHostApiImpl implements WebChromeClientHostApi { */ public static class WebChromeClientImpl extends SecureWebChromeClient { private final WebChromeClientFlutterApiImpl flutterApi; + private boolean returnValueForOnShowFileChooser = false; /** * Creates a {@link WebChromeClient} that passes arguments of callbacks methods to Dart. @@ -45,6 +49,36 @@ public WebChromeClientImpl(@NonNull WebChromeClientFlutterApiImpl flutterApi) { public void onProgressChanged(WebView view, int progress) { flutterApi.onProgressChanged(this, view, (long) progress, reply -> {}); } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean onShowFileChooser( + WebView webView, + ValueCallback filePathCallback, + FileChooserParams fileChooserParams) { + final boolean currentReturnValueForOnShowFileChooser = returnValueForOnShowFileChooser; + flutterApi.onShowFileChooser( + this, + webView, + fileChooserParams, + reply -> { + // The returned list of file paths can only be passed to `filePathCallback` if the + // `onShowFileChooser` method returned true. + if (currentReturnValueForOnShowFileChooser) { + final Uri[] filePaths = new Uri[reply.size()]; + for (int i = 0; i < reply.size(); i++) { + filePaths[i] = Uri.parse(reply.get(i)); + } + filePathCallback.onReceiveValue(filePaths); + } + }); + return currentReturnValueForOnShowFileChooser; + } + + /** Sets return value for {@link #onShowFileChooser}. */ + public void setReturnValueForOnShowFileChooser(boolean value) { + returnValueForOnShowFileChooser = value; + } } /** @@ -163,4 +197,12 @@ public void create(Long instanceId) { webChromeClientCreator.createWebChromeClient(flutterApi); instanceManager.addDartCreatedInstance(webChromeClient, instanceId); } + + @Override + public void setSynchronousReturnValueForOnShowFileChooser( + @NonNull Long instanceId, @NonNull Boolean value) { + final WebChromeClientImpl webChromeClient = + Objects.requireNonNull(instanceManager.getInstance(instanceId)); + webChromeClient.setReturnValueForOnShowFileChooser(value); + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApi.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApi.java new file mode 100644 index 000000000000..3819d7b26f62 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApi.java @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.WebView; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.FlutterEngine; + +/** + * App and package facing native API provided by the `webview_flutter_android` plugin. + * + *

This class follows the convention of breaking changes of the Dart API, which means that any + * changes to the class that are not backwards compatible will only be made with a major version + * change of the plugin. + * + *

Native code other than this external API does not follow breaking change conventions, so app + * or plugin clients should not use any other native APIs. + */ +@SuppressWarnings("unused") +public interface WebViewFlutterAndroidExternalApi { + /** + * Retrieves the {@link WebView} that is associated with `identifier`. + * + *

See the Dart method `AndroidWebViewController.webViewIdentifier` to get the identifier of an + * underlying `WebView`. + * + * @param engine the execution environment the {@link WebViewFlutterPlugin} should belong to. If + * the engine doesn't contain an attached instance of {@link WebViewFlutterPlugin}, this + * method returns null. + * @param identifier the associated identifier of the `WebView`. + * @return the `WebView` associated with `identifier` or null if a `WebView` instance associated + * with `identifier` could not be found. + */ + @Nullable + static WebView getWebView(FlutterEngine engine, long identifier) { + final WebViewFlutterPlugin webViewPlugin = + (WebViewFlutterPlugin) engine.getPlugins().get(WebViewFlutterPlugin.class); + + if (webViewPlugin != null && webViewPlugin.getInstanceManager() != null) { + final Object instance = webViewPlugin.getInstanceManager().getInstance(identifier); + if (instance instanceof WebView) { + return (WebView) instance; + } + } + + return null; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index 1c5a55057ca6..04a9735e0281 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -33,7 +33,7 @@ *

Call {@link #registerWith} to use the stable {@code io.flutter.plugin.common} package instead. */ public class WebViewFlutterPlugin implements FlutterPlugin, ActivityAware { - private InstanceManager instanceManager; + @Nullable private InstanceManager instanceManager; private FlutterPluginBinding pluginBinding; private WebViewHostApiImpl webViewHostApi; @@ -148,7 +148,10 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { @Override public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - instanceManager.close(); + if (instanceManager != null) { + instanceManager.close(); + instanceManager = null; + } } @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FileChooserParamsTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FileChooserParamsTest.java new file mode 100644 index 000000000000..3172ea4330c8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FileChooserParamsTest.java @@ -0,0 +1,74 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.webkit.WebChromeClient.FileChooserParams; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Arrays; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class FileChooserParamsTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public FileChooserParams mockFileChooserParams; + + @Mock public BinaryMessenger mockBinaryMessenger; + + InstanceManager instanceManager; + + @Before + public void setUp() { + instanceManager = InstanceManager.open(identifier -> {}); + } + + @After + public void tearDown() { + instanceManager.close(); + } + + @Test + public void flutterApiCreate() { + final FileChooserParamsFlutterApiImpl spyFlutterApi = + spy(new FileChooserParamsFlutterApiImpl(mockBinaryMessenger, instanceManager)); + + when(mockFileChooserParams.isCaptureEnabled()).thenReturn(true); + when(mockFileChooserParams.getAcceptTypes()).thenReturn(new String[] {"my", "list"}); + when(mockFileChooserParams.getMode()).thenReturn(FileChooserParams.MODE_OPEN_MULTIPLE); + when(mockFileChooserParams.getFilenameHint()).thenReturn("filenameHint"); + spyFlutterApi.create(mockFileChooserParams, reply -> {}); + + final long identifier = + Objects.requireNonNull( + instanceManager.getIdentifierForStrongReference(mockFileChooserParams)); + final ArgumentCaptor modeCaptor = + ArgumentCaptor.forClass(GeneratedAndroidWebView.FileChooserModeEnumData.class); + + verify(spyFlutterApi) + .create( + eq(identifier), + eq(true), + eq(Arrays.asList("my", "list")), + modeCaptor.capture(), + eq("filenameHint"), + any()); + assertEquals( + modeCaptor.getValue().getValue(), GeneratedAndroidWebView.FileChooserMode.OPEN_MULTIPLE); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterPluginTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApiTest.java similarity index 58% rename from packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterPluginTest.java rename to packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApiTest.java index 16dc6cf5de2b..0877dcaf2b06 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterPluginTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewFlutterAndroidExternalApiTest.java @@ -4,11 +4,16 @@ package io.flutter.plugins.webviewflutter; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import android.content.Context; +import android.webkit.WebView; +import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.PluginRegistry; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.platform.PlatformViewRegistry; import org.junit.Rule; @@ -17,7 +22,7 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -public class WebViewFlutterPluginTest { +public class WebViewFlutterAndroidExternalApiTest { @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); @Mock Context mockContext; @@ -29,7 +34,7 @@ public class WebViewFlutterPluginTest { @Mock FlutterPlugin.FlutterPluginBinding mockPluginBinding; @Test - public void getInstanceManagerAfterOnAttachedToEngine() { + public void getWebView() { final WebViewFlutterPlugin webViewFlutterPlugin = new WebViewFlutterPlugin(); when(mockPluginBinding.getApplicationContext()).thenReturn(mockContext); @@ -38,7 +43,19 @@ public void getInstanceManagerAfterOnAttachedToEngine() { webViewFlutterPlugin.onAttachedToEngine(mockPluginBinding); - assertNotNull(webViewFlutterPlugin.getInstanceManager()); + final InstanceManager instanceManager = webViewFlutterPlugin.getInstanceManager(); + assertNotNull(instanceManager); + + final WebView mockWebView = mock(WebView.class); + instanceManager.addDartCreatedInstance(mockWebView, 0); + + final PluginRegistry mockPluginRegistry = mock(PluginRegistry.class); + when(mockPluginRegistry.get(WebViewFlutterPlugin.class)).thenReturn(webViewFlutterPlugin); + + final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class); + when(mockFlutterEngine.getPlugins()).thenReturn(mockPluginRegistry); + + assertEquals(WebViewFlutterAndroidExternalApi.getWebView(mockFlutterEngine, 0), mockWebView); webViewFlutterPlugin.onDetachedFromEngine(mockPluginBinding); } diff --git a/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties b/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties index 94adc3a3f97a..598d13fee446 100644 --- a/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties +++ b/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/legacy/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/legacy/webview_flutter_test.dart index 180175a22a0a..cbec6b767952 100644 --- a/packages/webview_flutter/webview_flutter_android/example/integration_test/legacy/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/legacy/webview_flutter_test.dart @@ -1557,7 +1557,7 @@ class CopyableObjectWithCallback with Copyable { class ClassWithCallbackClass { ClassWithCallbackClass() { callbackClass = CopyableObjectWithCallback( - withWeakRefenceTo( + withWeakReferenceTo( this, (WeakReference weakReference) { return () { diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart index f6a55b1f2795..af144e55efba 100644 --- a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart @@ -17,6 +17,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_android/src/android_webview.dart' as android; +import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; import 'package:webview_flutter_android/src/instance_manager.dart'; import 'package:webview_flutter_android/src/weak_reference_utils.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; @@ -58,15 +60,13 @@ Future main() async { ) ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageFinished.future; @@ -96,6 +96,79 @@ Future main() async { expect(gcIdentifier, 0); }, timeout: const Timeout(Duration(seconds: 10))); + testWidgets( + 'WebView is released by garbage collection', + (WidgetTester tester) async { + final Completer webViewGCCompleter = Completer(); + + late final InstanceManager instanceManager; + instanceManager = + InstanceManager(onWeakReferenceRemoved: (int identifier) { + final Copyable instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + if (instance is android.WebView && !webViewGCCompleter.isCompleted) { + webViewGCCompleter.complete(); + } + }); + + android.WebView.api = WebViewHostApiImpl( + instanceManager: instanceManager, + ); + android.WebSettings.api = WebSettingsHostApiImpl( + instanceManager: instanceManager, + ); + android.WebChromeClient.api = WebChromeClientHostApiImpl( + instanceManager: instanceManager, + ); + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + AndroidWebViewWidgetCreationParams( + instanceManager: instanceManager, + controller: PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ), + ), + ).build(context); + }, + ), + ); + await tester.pumpAndSettle(); + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + AndroidWebViewWidgetCreationParams( + instanceManager: instanceManager, + controller: PlatformWebViewController( + const PlatformWebViewControllerCreationParams(), + ), + ), + ).build(context); + }, + ), + ); + await tester.pumpAndSettle(); + + // Force garbage collection. + await IntegrationTestWidgetsFlutterBinding.instance + .watchPerformance(() async { + await tester.pumpAndSettle(); + }); + + await tester.pumpAndSettle(); + await expectLater(webViewGCCompleter.future, completes); + + android.WebView.api = WebViewHostApiImpl(); + android.WebSettings.api = WebSettingsHostApiImpl(); + android.WebChromeClient.api = WebChromeClientHostApiImpl(); + }, + timeout: const Timeout(Duration(seconds: 10)), + ); + testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { final Completer pageFinished = Completer(); @@ -110,15 +183,13 @@ Future main() async { ) ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageFinished.future; @@ -151,15 +222,13 @@ Future main() async { ), ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoads.stream.firstWhere((String url) => url == headersUrl); @@ -195,15 +264,13 @@ Future main() async { 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageFinished.future; @@ -258,15 +325,13 @@ Future main() async { ..setUserAgent('Custom_User_Agent1') ..loadRequest(LoadRequestParams(uri: Uri.parse('about:blank'))); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageFinished.future; @@ -335,15 +400,13 @@ Future main() async { ), ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -369,15 +432,13 @@ Future main() async { ), ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -491,15 +552,13 @@ Future main() async { ), ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -525,15 +584,13 @@ Future main() async { ), ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -573,15 +630,13 @@ Future main() async { ), ); - await tester.pumpWidget( - Builder( - builder: (BuildContext context) { - return PlatformWebViewWidget( - PlatformWebViewWidgetCreationParams(controller: controller), - ).build(context); - }, - ), - ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); await pageLoaded.future; @@ -1181,7 +1236,7 @@ class CopyableObjectWithCallback with Copyable { class ClassWithCallbackClass { ClassWithCallbackClass() { callbackClass = CopyableObjectWithCallback( - withWeakRefenceTo( + withWeakReferenceTo( this, (WeakReference weakReference) { return () { diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index fe6d723c058f..75f01b457b3a 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -164,9 +164,11 @@ Page resource error: return FloatingActionButton( onPressed: () async { final String? url = await _controller.currentUrl(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Favorited $url')), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + } }, child: const Icon(Icons.favorite), ); @@ -319,25 +321,29 @@ class SampleMenu extends StatelessWidget { Future _onListCookies(BuildContext context) async { final String cookies = await webViewController .runJavaScriptReturningResult('document.cookie') as String; - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Cookies:'), - _getCookieList(cookies), - ], - ), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } } Future _onAddToCache(BuildContext context) async { await webViewController.runJavaScript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";', ); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Added a test entry to cache.'), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } } Future _onListCache() { @@ -350,9 +356,11 @@ class SampleMenu extends StatelessWidget { Future _onClearCache(BuildContext context) async { await webViewController.clearCache(); await webViewController.clearLocalStorage(); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Cache cleared.'), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Cache cleared.'), + )); + } } Future _onClearCookies(BuildContext context) async { @@ -361,9 +369,11 @@ class SampleMenu extends StatelessWidget { if (!hadCookies) { message = 'There are no cookies.'; } - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(message), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } } Future _onNavigationDelegateExample() { @@ -462,10 +472,11 @@ class NavigationControls extends StatelessWidget { if (await webViewController.canGoBack()) { await webViewController.goBack(); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No back history item')), - ); - return; + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No back history item')), + ); + } } }, ), @@ -475,10 +486,11 @@ class NavigationControls extends StatelessWidget { if (await webViewController.canGoForward()) { await webViewController.goForward(); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No forward history item')), - ); - return; + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No forward history item')), + ); + } } }, ), diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml index 0daacb07b13f..0fc0daf84118 100644 --- a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -4,6 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_navigation_delegate.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_navigation_delegate.dart deleted file mode 100644 index 51c62764fde4..000000000000 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_navigation_delegate.dart +++ /dev/null @@ -1,318 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; - -import 'android_proxy.dart'; -import 'android_webview.dart' as android_webview; - -/// Signature for the `loadRequest` callback responsible for loading the [url] -/// after a navigation request has been approved. -typedef LoadRequestCallback = Future Function(LoadRequestParams params); - -/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. -@immutable -class AndroidWebResourceError extends WebResourceError { - /// Creates a new [AndroidWebResourceError]. - AndroidWebResourceError._({ - required super.errorCode, - required super.description, - super.isForMainFrame, - this.failingUrl, - }) : super( - errorType: _errorCodeToErrorType(errorCode), - ); - - /// Gets the URL for which the failing resource request was made. - final String? failingUrl; - - static WebResourceErrorType? _errorCodeToErrorType(int errorCode) { - switch (errorCode) { - case android_webview.WebViewClient.errorAuthentication: - return WebResourceErrorType.authentication; - case android_webview.WebViewClient.errorBadUrl: - return WebResourceErrorType.badUrl; - case android_webview.WebViewClient.errorConnect: - return WebResourceErrorType.connect; - case android_webview.WebViewClient.errorFailedSslHandshake: - return WebResourceErrorType.failedSslHandshake; - case android_webview.WebViewClient.errorFile: - return WebResourceErrorType.file; - case android_webview.WebViewClient.errorFileNotFound: - return WebResourceErrorType.fileNotFound; - case android_webview.WebViewClient.errorHostLookup: - return WebResourceErrorType.hostLookup; - case android_webview.WebViewClient.errorIO: - return WebResourceErrorType.io; - case android_webview.WebViewClient.errorProxyAuthentication: - return WebResourceErrorType.proxyAuthentication; - case android_webview.WebViewClient.errorRedirectLoop: - return WebResourceErrorType.redirectLoop; - case android_webview.WebViewClient.errorTimeout: - return WebResourceErrorType.timeout; - case android_webview.WebViewClient.errorTooManyRequests: - return WebResourceErrorType.tooManyRequests; - case android_webview.WebViewClient.errorUnknown: - return WebResourceErrorType.unknown; - case android_webview.WebViewClient.errorUnsafeResource: - return WebResourceErrorType.unsafeResource; - case android_webview.WebViewClient.errorUnsupportedAuthScheme: - return WebResourceErrorType.unsupportedAuthScheme; - case android_webview.WebViewClient.errorUnsupportedScheme: - return WebResourceErrorType.unsupportedScheme; - } - - throw ArgumentError( - 'Could not find a WebResourceErrorType for errorCode: $errorCode', - ); - } -} - -/// Object specifying creation parameters for creating a [AndroidNavigationDelegate]. -/// -/// When adding additional fields make sure they can be null or have a default -/// value to avoid breaking changes. See [PlatformNavigationDelegateCreationParams] for -/// more information. -@immutable -class AndroidNavigationDelegateCreationParams - extends PlatformNavigationDelegateCreationParams { - /// Creates a new [AndroidNavigationDelegateCreationParams] instance. - const AndroidNavigationDelegateCreationParams._({ - @visibleForTesting this.androidWebViewProxy = const AndroidWebViewProxy(), - }) : super(); - - /// Creates a [AndroidNavigationDelegateCreationParams] instance based on [PlatformNavigationDelegateCreationParams]. - factory AndroidNavigationDelegateCreationParams.fromPlatformNavigationDelegateCreationParams( - // Recommended placeholder to prevent being broken by platform interface. - // ignore: avoid_unused_constructor_parameters - PlatformNavigationDelegateCreationParams params, { - @visibleForTesting - AndroidWebViewProxy androidWebViewProxy = const AndroidWebViewProxy(), - }) { - return AndroidNavigationDelegateCreationParams._( - androidWebViewProxy: androidWebViewProxy, - ); - } - - /// Handles constructing objects and calling static methods for the Android WebView - /// native library. - @visibleForTesting - final AndroidWebViewProxy androidWebViewProxy; -} - -/// A place to register callback methods responsible to handle navigation events -/// triggered by the [android_webview.WebView]. -class AndroidNavigationDelegate extends PlatformNavigationDelegate { - /// Creates a new [AndroidNavigationkDelegate]. - AndroidNavigationDelegate(PlatformNavigationDelegateCreationParams params) - : super.implementation(params is AndroidNavigationDelegateCreationParams - ? params - : AndroidNavigationDelegateCreationParams - .fromPlatformNavigationDelegateCreationParams(params)) { - final WeakReference weakThis = - WeakReference(this); - - _webChromeClient = (this.params as AndroidNavigationDelegateCreationParams) - .androidWebViewProxy - .createAndroidWebChromeClient( - onProgressChanged: (android_webview.WebView webView, int progress) { - if (weakThis.target?._onProgress != null) { - weakThis.target!._onProgress!(progress); - } - }); - - _webViewClient = (this.params as AndroidNavigationDelegateCreationParams) - .androidWebViewProxy - .createAndroidWebViewClient( - onPageFinished: (android_webview.WebView webView, String url) { - if (weakThis.target?._onPageFinished != null) { - weakThis.target!._onPageFinished!(url); - } - }, - onPageStarted: (android_webview.WebView webView, String url) { - if (weakThis.target?._onPageStarted != null) { - weakThis.target!._onPageStarted!(url); - } - }, - onReceivedRequestError: ( - android_webview.WebView webView, - android_webview.WebResourceRequest request, - android_webview.WebResourceError error, - ) { - if (weakThis.target?._onWebResourceError != null) { - weakThis.target!._onWebResourceError!(AndroidWebResourceError._( - errorCode: error.errorCode, - description: error.description, - failingUrl: request.url, - isForMainFrame: request.isForMainFrame, - )); - } - }, - onReceivedError: ( - android_webview.WebView webView, - int errorCode, - String description, - String failingUrl, - ) { - if (weakThis.target?._onWebResourceError != null) { - weakThis.target!._onWebResourceError!(AndroidWebResourceError._( - errorCode: errorCode, - description: description, - failingUrl: failingUrl, - isForMainFrame: true, - )); - } - }, - requestLoading: ( - android_webview.WebView webView, - android_webview.WebResourceRequest request, - ) { - if (weakThis.target != null) { - weakThis.target!._handleNavigation( - request.url, - headers: request.requestHeaders, - isForMainFrame: request.isForMainFrame, - ); - } - }, - urlLoading: ( - android_webview.WebView webView, - String url, - ) { - if (weakThis.target != null) { - weakThis.target!._handleNavigation(url, isForMainFrame: true); - } - }, - ); - - _downloadListener = (this.params as AndroidNavigationDelegateCreationParams) - .androidWebViewProxy - .createDownloadListener( - onDownloadStart: ( - String url, - String userAgent, - String contentDisposition, - String mimetype, - int contentLength, - ) { - if (weakThis.target != null) { - weakThis.target?._handleNavigation(url, isForMainFrame: true); - } - }, - ); - } - - late final android_webview.WebChromeClient _webChromeClient; - - /// Gets the native [android_webview.WebChromeClient] that is bridged by this [AndroidNavigationDelegate]. - /// - /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setWebChromeClient`. - android_webview.WebChromeClient get androidWebChromeClient => - _webChromeClient; - - late final android_webview.WebViewClient _webViewClient; - - /// Gets the native [android_webview.WebViewClient] that is bridged by this [AndroidNavigationDelegate]. - /// - /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setWebViewClient`. - android_webview.WebViewClient get androidWebViewClient => _webViewClient; - - late final android_webview.DownloadListener _downloadListener; - - /// Gets the native [android_webview.DownloadListener] that is bridged by this [AndroidNavigationDelegate]. - /// - /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setDownloadListener`. - android_webview.DownloadListener get androidDownloadListener => - _downloadListener; - - PageEventCallback? _onPageFinished; - PageEventCallback? _onPageStarted; - ProgressCallback? _onProgress; - WebResourceErrorCallback? _onWebResourceError; - NavigationRequestCallback? _onNavigationRequest; - LoadRequestCallback? _onLoadRequest; - - void _handleNavigation( - String url, { - required bool isForMainFrame, - Map headers = const {}, - }) { - final LoadRequestCallback? onLoadRequest = _onLoadRequest; - final NavigationRequestCallback? onNavigationRequest = _onNavigationRequest; - - if (onNavigationRequest == null || onLoadRequest == null) { - return; - } - - final FutureOr returnValue = onNavigationRequest( - NavigationRequest( - url: url, - isMainFrame: isForMainFrame, - ), - ); - - if (returnValue is NavigationDecision && - returnValue == NavigationDecision.navigate) { - onLoadRequest(LoadRequestParams( - uri: Uri.parse(url), - headers: headers, - )); - } else if (returnValue is Future) { - returnValue.then((NavigationDecision shouldLoadUrl) { - if (shouldLoadUrl == NavigationDecision.navigate) { - onLoadRequest(LoadRequestParams( - uri: Uri.parse(url), - headers: headers, - )); - } - }); - } - } - - /// Invoked when loading the url after a navigation request is approved. - Future setOnLoadRequest( - LoadRequestCallback onLoadRequest, - ) async { - _onLoadRequest = onLoadRequest; - } - - @override - Future setOnNavigationRequest( - NavigationRequestCallback onNavigationRequest, - ) async { - _onNavigationRequest = onNavigationRequest; - _webViewClient.setSynchronousReturnValueForShouldOverrideUrlLoading(true); - } - - @override - Future setOnPageStarted( - PageEventCallback onPageStarted, - ) async { - _onPageStarted = onPageStarted; - } - - @override - Future setOnPageFinished( - PageEventCallback onPageFinished, - ) async { - _onPageFinished = onPageFinished; - } - - @override - Future setOnProgress( - ProgressCallback onProgress, - ) async { - _onProgress = onProgress; - } - - @override - Future setOnWebResourceError( - WebResourceErrorCallback onWebResourceError, - ) async { - _onWebResourceError = onWebResourceError; - } -} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart index db247ee41d1c..9437e9dd3eb4 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_proxy.dart @@ -37,6 +37,11 @@ class AndroidWebViewProxy { final android_webview.WebChromeClient Function({ void Function(android_webview.WebView webView, int progress)? onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? + onShowFileChooser, }) createAndroidWebChromeClient; /// Constructs a [android_webview.WebViewClient]. diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart index 66f93dde1679..1ab30a9ea1fd 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.dart @@ -16,10 +16,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart' show BinaryMessenger; import 'package:flutter/widgets.dart' show AndroidViewSurface; -import 'android_webview.pigeon.dart'; +import 'android_webview.g.dart'; import 'android_webview_api_impls.dart'; import 'instance_manager.dart'; +export 'android_webview_api_impls.dart' show FileChooserMode; + /// Root of the Java class hierarchy. /// /// See https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html. @@ -871,7 +873,8 @@ class DownloadListener extends JavaObject { /// Handles JavaScript dialogs, favicons, titles, and the progress for [WebView]. class WebChromeClient extends JavaObject { /// Constructs a [WebChromeClient]. - WebChromeClient({this.onProgressChanged}) : super.detached() { + WebChromeClient({this.onProgressChanged, this.onShowFileChooser}) + : super.detached() { AndroidWebViewFlutterApis.instance.ensureSetUp(); api.createFromInstance(this); } @@ -881,7 +884,10 @@ class WebChromeClient extends JavaObject { /// /// This should only be used by subclasses created by this library or to /// create copies. - WebChromeClient.detached({this.onProgressChanged}) : super.detached(); + WebChromeClient.detached({ + this.onProgressChanged, + this.onShowFileChooser, + }) : super.detached(); /// Pigeon Host Api implementation for [WebChromeClient]. @visibleForTesting @@ -890,9 +896,95 @@ class WebChromeClient extends JavaObject { /// Notify the host application that a file should be downloaded. final void Function(WebView webView, int progress)? onProgressChanged; + /// Indicates the client should show a file chooser. + /// + /// To handle the request for a file chooser with this callback, passing true + /// to [setSynchronousReturnValueForOnShowFileChooser] is required. Otherwise, + /// the returned list of strings will be ignored and the client will use the + /// default handling of a file chooser request. + /// + /// Only invoked on Android versions 21+. + final Future> Function( + WebView webView, + FileChooserParams params, + )? onShowFileChooser; + + /// Sets the required synchronous return value for the Java method, + /// `WebChromeClient.onShowFileChooser(...)`. + /// + /// The Java method, `WebChromeClient.onShowFileChooser(...)`, requires + /// a boolean to be returned and this method sets the returned value for all + /// calls to the Java method. + /// + /// Setting this to true indicates that all file chooser requests should be + /// handled by [onShowFileChooser] and the returned list of Strings will be + /// returned to the WebView. Otherwise, the client will use the default + /// handling and the returned value in [onShowFileChooser] will be ignored. + /// + /// Requires [onShowFileChooser] to be nonnull. + /// + /// Defaults to false. + Future setSynchronousReturnValueForOnShowFileChooser( + bool value, + ) { + if (value && onShowFileChooser == null) { + throw StateError( + 'Setting this to true requires `onShowFileChooser` to be nonnull.', + ); + } + return api.setSynchronousReturnValueForOnShowFileChooserFromInstance( + this, + value, + ); + } + @override WebChromeClient copy() { - return WebChromeClient.detached(onProgressChanged: onProgressChanged); + return WebChromeClient.detached( + onProgressChanged: onProgressChanged, + onShowFileChooser: onShowFileChooser, + ); + } +} + +/// Parameters received when a [WebChromeClient] should show a file chooser. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. +class FileChooserParams extends JavaObject { + /// Constructs a [FileChooserParams] without creating the associated Java + /// object. + /// + /// This should only be used by subclasses created by this library or to + /// create copies. + FileChooserParams.detached({ + required this.isCaptureEnabled, + required this.acceptTypes, + required this.filenameHint, + required this.mode, + super.binaryMessenger, + super.instanceManager, + }) : super.detached(); + + /// Preference for a live media captured value (e.g. Camera, Microphone). + final bool isCaptureEnabled; + + /// A list of acceptable MIME types. + final List acceptTypes; + + /// The file name of a default selection if specified, or null. + final String? filenameHint; + + /// Mode of how to select files for a file chooser. + final FileChooserMode mode; + + @override + FileChooserParams copy() { + return FileChooserParams.detached( + isCaptureEnabled: isCaptureEnabled, + acceptTypes: acceptTypes, + filenameHint: filenameHint, + mode: mode, + ); } } diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart similarity index 68% rename from packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart rename to packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart index 5bdab16d6720..d3c306a10238 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v4.2.3), do not edit directly. +// Autogenerated from Pigeon (v4.2.14), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import import 'dart:async'; @@ -10,6 +10,48 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +/// Mode of how to select files for a file chooser. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. +enum FileChooserMode { + /// Open single file and requires that the file exists before allowing the + /// user to pick it. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_OPEN. + open, + + /// Similar to [open] but allows multiple files to be selected. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_OPEN_MULTIPLE. + openMultiple, + + /// Allows picking a nonexistent file and saving it. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_SAVE. + save, +} + +class FileChooserModeEnumData { + FileChooserModeEnumData({ + required this.value, + }); + + FileChooserMode value; + + Object encode() { + return [ + value.index, + ]; + } + + static FileChooserModeEnumData decode(Object result) { + result as List; + return FileChooserModeEnumData( + value: FileChooserMode.values[result[0]! as int], + ); + } +} + class WebResourceRequestData { WebResourceRequestData({ required this.url, @@ -21,33 +63,38 @@ class WebResourceRequestData { }); String url; + bool isForMainFrame; + bool? isRedirect; + bool hasGesture; + String method; + Map requestHeaders; Object encode() { - final Map pigeonMap = {}; - pigeonMap['url'] = url; - pigeonMap['isForMainFrame'] = isForMainFrame; - pigeonMap['isRedirect'] = isRedirect; - pigeonMap['hasGesture'] = hasGesture; - pigeonMap['method'] = method; - pigeonMap['requestHeaders'] = requestHeaders; - return pigeonMap; - } - - static WebResourceRequestData decode(Object message) { - final Map pigeonMap = message as Map; + return [ + url, + isForMainFrame, + isRedirect, + hasGesture, + method, + requestHeaders, + ]; + } + + static WebResourceRequestData decode(Object result) { + result as List; return WebResourceRequestData( - url: pigeonMap['url']! as String, - isForMainFrame: pigeonMap['isForMainFrame']! as bool, - isRedirect: pigeonMap['isRedirect'] as bool?, - hasGesture: pigeonMap['hasGesture']! as bool, - method: pigeonMap['method']! as String, - requestHeaders: (pigeonMap['requestHeaders'] as Map?)! - .cast(), + url: result[0]! as String, + isForMainFrame: result[1]! as bool, + isRedirect: result[2] as bool?, + hasGesture: result[3]! as bool, + method: result[4]! as String, + requestHeaders: + (result[5] as Map?)!.cast(), ); } } @@ -59,20 +106,21 @@ class WebResourceErrorData { }); int errorCode; + String description; Object encode() { - final Map pigeonMap = {}; - pigeonMap['errorCode'] = errorCode; - pigeonMap['description'] = description; - return pigeonMap; + return [ + errorCode, + description, + ]; } - static WebResourceErrorData decode(Object message) { - final Map pigeonMap = message as Map; + static WebResourceErrorData decode(Object result) { + result as List; return WebResourceErrorData( - errorCode: pigeonMap['errorCode']! as int, - description: pigeonMap['description']! as String, + errorCode: result[0]! as int, + description: result[1]! as String, ); } } @@ -84,20 +132,21 @@ class WebViewPoint { }); int x; + int y; Object encode() { - final Map pigeonMap = {}; - pigeonMap['x'] = x; - pigeonMap['y'] = y; - return pigeonMap; + return [ + x, + y, + ]; } - static WebViewPoint decode(Object message) { - final Map pigeonMap = message as Map; + static WebViewPoint decode(Object result) { + result as List; return WebViewPoint( - x: pigeonMap['x']! as int, - y: pigeonMap['y']! as int, + x: result[0]! as int, + y: result[1]! as int, ); } } @@ -121,20 +170,18 @@ class JavaObjectHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.JavaObjectHostApi.dispose', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_identifier]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_identifier]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -149,6 +196,7 @@ abstract class JavaObjectFlutterApi { static const MessageCodec codec = StandardMessageCodec(); void dispose(int identifier); + static void setup(JavaObjectFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -187,28 +235,25 @@ class CookieManagerHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.CookieManagerHostApi.clearCookies', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send(null) as Map?; - if (replyMap == null) { + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as bool?)!; + return (replyList[0] as bool?)!; } } @@ -216,20 +261,18 @@ class CookieManagerHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.CookieManagerHostApi.setCookie', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_url, arg_value]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_url, arg_value]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -275,21 +318,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_useHybridComposition]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -301,21 +342,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.loadData', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel.send( + final List? replyList = await channel.send( [arg_instanceId, arg_data, arg_mimeType, arg_encoding]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -332,26 +371,24 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.loadDataWithBaseUrl', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel.send([ + final List? replyList = await channel.send([ arg_instanceId, arg_baseUrl, arg_data, arg_mimeType, arg_encoding, arg_historyUrl - ]) as Map?; - if (replyMap == null) { + ]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -363,21 +400,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.loadUrl', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_url, arg_headers]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -389,21 +424,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.postUrl', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId, arg_url, arg_data]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_url, arg_data]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -414,23 +446,21 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getUrl', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { - return (replyMap['result'] as String?); + return (replyList[0] as String?); } } @@ -438,28 +468,26 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.canGoBack', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as bool?)!; + return (replyList[0] as bool?)!; } } @@ -467,28 +495,26 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.canGoForward', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as bool?)!; + return (replyList[0] as bool?)!; } } @@ -496,20 +522,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.goBack', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -520,20 +544,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.goForward', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -544,20 +566,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.reload', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -568,21 +588,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.clearCache', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_includeDiskFiles]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -594,24 +612,22 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.evaluateJavascript', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_javascriptString]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { - return (replyMap['result'] as String?); + return (replyList[0] as String?); } } @@ -619,23 +635,21 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getTitle', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { - return (replyMap['result'] as String?); + return (replyList[0] as String?); } } @@ -643,21 +657,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.scrollTo', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId, arg_x, arg_y]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_x, arg_y]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -668,21 +679,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.scrollBy', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId, arg_x, arg_y]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_x, arg_y]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -693,28 +701,26 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getScrollX', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as int?)!; + return (replyList[0] as int?)!; } } @@ -722,28 +728,26 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getScrollY', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as int?)!; + return (replyList[0] as int?)!; } } @@ -751,28 +755,26 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.getScrollPosition', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as WebViewPoint?)!; + return (replyList[0] as WebViewPoint?)!; } } @@ -781,20 +783,18 @@ class WebViewHostApi { 'dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_enabled]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_enabled]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -806,21 +806,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.setWebViewClient', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_instanceId, arg_webViewClientInstanceId]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -832,21 +830,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_instanceId, arg_javaScriptChannelInstanceId]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -858,21 +854,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel + final List? replyList = await channel .send([arg_instanceId, arg_javaScriptChannelInstanceId]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -884,21 +878,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.setDownloadListener', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_listenerInstanceId]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -910,21 +902,19 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.setWebChromeClient', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_clientInstanceId]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -935,20 +925,18 @@ class WebViewHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewHostApi.setBackgroundColor', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_color]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_color]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -970,21 +958,19 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = + final List? replyList = await channel.send([arg_instanceId, arg_webViewInstanceId]) - as Map?; - if (replyMap == null) { + as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -995,20 +981,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_flag]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_flag]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1021,20 +1005,18 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_flag]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_flag]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1047,20 +1029,18 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_support]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_support]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1071,20 +1051,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_flag]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_flag]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1096,21 +1074,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId, arg_userAgentString]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_userAgentString]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1123,20 +1098,18 @@ class WebSettingsHostApi { 'dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_require]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_require]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1147,20 +1120,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_support]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_support]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1172,21 +1143,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId, arg_overview]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_overview]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1197,20 +1165,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_use]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_use]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1222,20 +1188,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_enabled]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1247,20 +1211,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_enabled]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1271,20 +1233,18 @@ class WebSettingsHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_enabled]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_enabled]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1306,21 +1266,18 @@ class JavaScriptChannelHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.JavaScriptChannelHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId, arg_channelName]) - as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_channelName]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1332,6 +1289,7 @@ abstract class JavaScriptChannelFlutterApi { static const MessageCodec codec = StandardMessageCodec(); void postMessage(int instanceId, String message); + static void setup(JavaScriptChannelFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1373,20 +1331,18 @@ class WebViewClientHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebViewClientHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1399,20 +1355,18 @@ class WebViewClientHostApi { 'dev.flutter.pigeon.WebViewClientHostApi.setSynchronousReturnValueForShouldOverrideUrlLoading', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = await channel - .send([arg_instanceId, arg_value]) as Map?; - if (replyMap == null) { + final List? replyList = await channel + .send([arg_instanceId, arg_value]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1454,14 +1408,20 @@ abstract class WebViewClientFlutterApi { static const MessageCodec codec = _WebViewClientFlutterApiCodec(); void onPageStarted(int instanceId, int webViewInstanceId, String url); + void onPageFinished(int instanceId, int webViewInstanceId, String url); + void onReceivedRequestError(int instanceId, int webViewInstanceId, WebResourceRequestData request, WebResourceErrorData error); + void onReceivedError(int instanceId, int webViewInstanceId, int errorCode, String description, String failingUrl); + void requestLoading( int instanceId, int webViewInstanceId, WebResourceRequestData request); + void urlLoading(int instanceId, int webViewInstanceId, String url); + static void setup(WebViewClientFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1647,20 +1607,18 @@ class DownloadListenerHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.DownloadListenerHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1673,6 +1631,7 @@ abstract class DownloadListenerFlutterApi { void onDownloadStart(int instanceId, String url, String userAgent, String contentDisposition, String mimetype, int contentLength); + static void setup(DownloadListenerFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1728,20 +1687,42 @@ class WebChromeClientHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebChromeClientHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + Future setSynchronousReturnValueForOnShowFileChooser( + int arg_instanceId, bool arg_value) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientHostApi.setSynchronousReturnValueForOnShowFileChooser', + codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_instanceId, arg_value]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1763,28 +1744,26 @@ class FlutterAssetManagerHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.FlutterAssetManagerHostApi.list', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_path]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_path]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as List?)!.cast(); + return (replyList[0] as List?)!.cast(); } } @@ -1793,28 +1772,26 @@ class FlutterAssetManagerHostApi { 'dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_name]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_name]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); - } else if (replyMap['result'] == null) { + } else if (replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyMap['result'] as String?)!; + return (replyList[0] as String?)!; } } } @@ -1823,6 +1800,10 @@ abstract class WebChromeClientFlutterApi { static const MessageCodec codec = StandardMessageCodec(); void onProgressChanged(int instanceId, int webViewInstanceId, int progress); + + Future> onShowFileChooser( + int instanceId, int webViewInstanceId, int paramsInstanceId); + static void setup(WebChromeClientFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1852,6 +1833,33 @@ abstract class WebChromeClientFlutterApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientFlutterApi.onShowFileChooser', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onShowFileChooser was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onShowFileChooser was null, expected non-null int.'); + final int? arg_webViewInstanceId = (args[1] as int?); + assert(arg_webViewInstanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onShowFileChooser was null, expected non-null int.'); + final int? arg_paramsInstanceId = (args[2] as int?); + assert(arg_paramsInstanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientFlutterApi.onShowFileChooser was null, expected non-null int.'); + final List output = await api.onShowFileChooser( + arg_instanceId!, arg_webViewInstanceId!, arg_paramsInstanceId!); + return output; + }); + } + } } } @@ -1869,20 +1877,18 @@ class WebStorageHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebStorageHostApi.create', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; @@ -1893,23 +1899,92 @@ class WebStorageHostApi { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.WebStorageHostApi.deleteAllData', codec, binaryMessenger: _binaryMessenger); - final Map? replyMap = - await channel.send([arg_instanceId]) as Map?; - if (replyMap == null) { + final List? replyList = + await channel.send([arg_instanceId]) as List?; + if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', ); - } else if (replyMap['error'] != null) { - final Map error = - (replyMap['error'] as Map?)!; + } else if (replyList.length > 1) { throw PlatformException( - code: (error['code'] as String?)!, - message: error['message'] as String?, - details: error['details'], + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], ); } else { return; } } } + +class _FileChooserParamsFlutterApiCodec extends StandardMessageCodec { + const _FileChooserParamsFlutterApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is FileChooserModeEnumData) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return FileChooserModeEnumData.decode(readValue(buffer)!); + + default: + return super.readValueOfType(type, buffer); + } + } +} + +/// Handles callbacks methods for the native Java FileChooserParams class. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. +abstract class FileChooserParamsFlutterApi { + static const MessageCodec codec = + _FileChooserParamsFlutterApiCodec(); + + void create(int instanceId, bool isCaptureEnabled, List acceptTypes, + FileChooserModeEnumData mode, String? filenameHint); + + static void setup(FileChooserParamsFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.FileChooserParamsFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.FileChooserParamsFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.FileChooserParamsFlutterApi.create was null, expected non-null int.'); + final bool? arg_isCaptureEnabled = (args[1] as bool?); + assert(arg_isCaptureEnabled != null, + 'Argument for dev.flutter.pigeon.FileChooserParamsFlutterApi.create was null, expected non-null bool.'); + final List? arg_acceptTypes = + (args[2] as List?)?.cast(); + assert(arg_acceptTypes != null, + 'Argument for dev.flutter.pigeon.FileChooserParamsFlutterApi.create was null, expected non-null List.'); + final FileChooserModeEnumData? arg_mode = + (args[3] as FileChooserModeEnumData?); + assert(arg_mode != null, + 'Argument for dev.flutter.pigeon.FileChooserParamsFlutterApi.create was null, expected non-null FileChooserModeEnumData.'); + final String? arg_filenameHint = (args[4] as String?); + api.create(arg_instanceId!, arg_isCaptureEnabled!, arg_acceptTypes!, + arg_mode!, arg_filenameHint); + return; + }); + } + } + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart index 8aa5f7d9dab4..127a2fa58ef8 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_api_impls.dart @@ -10,9 +10,11 @@ import 'dart:ui'; import 'package:flutter/services.dart' show BinaryMessenger; import 'android_webview.dart'; -import 'android_webview.pigeon.dart'; +import 'android_webview.g.dart'; import 'instance_manager.dart'; +export 'android_webview.g.dart' show FileChooserMode; + /// Converts [WebResourceRequestData] to [WebResourceRequest] WebResourceRequest _toWebResourceRequest(WebResourceRequestData data) { return WebResourceRequest( @@ -42,6 +44,7 @@ class AndroidWebViewFlutterApis { WebViewClientFlutterApiImpl? webViewClientFlutterApi, WebChromeClientFlutterApiImpl? webChromeClientFlutterApi, JavaScriptChannelFlutterApiImpl? javaScriptChannelFlutterApi, + FileChooserParamsFlutterApiImpl? fileChooserParamsFlutterApi, }) { this.javaObjectFlutterApi = javaObjectFlutterApi ?? JavaObjectFlutterApiImpl(); @@ -53,6 +56,8 @@ class AndroidWebViewFlutterApis { webChromeClientFlutterApi ?? WebChromeClientFlutterApiImpl(); this.javaScriptChannelFlutterApi = javaScriptChannelFlutterApi ?? JavaScriptChannelFlutterApiImpl(); + this.fileChooserParamsFlutterApi = + fileChooserParamsFlutterApi ?? FileChooserParamsFlutterApiImpl(); } static bool _haveBeenSetUp = false; @@ -77,6 +82,9 @@ class AndroidWebViewFlutterApis { /// Flutter Api for [JavaScriptChannel]. late final JavaScriptChannelFlutterApiImpl javaScriptChannelFlutterApi; + /// Flutter Api for [FileChooserParams]. + late final FileChooserParamsFlutterApiImpl fileChooserParamsFlutterApi; + /// Ensures all the Flutter APIs have been setup to receive calls from native code. void ensureSetUp() { if (!_haveBeenSetUp) { @@ -85,6 +93,7 @@ class AndroidWebViewFlutterApis { WebViewClientFlutterApi.setup(webViewClientFlutterApi); WebChromeClientFlutterApi.setup(webChromeClientFlutterApi); JavaScriptChannelFlutterApi.setup(javaScriptChannelFlutterApi); + FileChooserParamsFlutterApi.setup(fileChooserParamsFlutterApi); _haveBeenSetUp = true; } } @@ -781,6 +790,17 @@ class WebChromeClientHostApiImpl extends WebChromeClientHostApi { return create(identifier); } } + + /// Helper method to convert instances ids to objects. + Future setSynchronousReturnValueForOnShowFileChooserFromInstance( + WebChromeClient instance, + bool value, + ) { + return setSynchronousReturnValueForOnShowFileChooser( + instanceManager.getIdentifier(instance)!, + value, + ); + } } /// Flutter api implementation for [DownloadListener]. @@ -810,6 +830,26 @@ class WebChromeClientFlutterApiImpl extends WebChromeClientFlutterApi { instance.onProgressChanged!(webViewInstance!, progress); } } + + @override + Future> onShowFileChooser( + int instanceId, + int webViewInstanceId, + int paramsInstanceId, + ) { + final WebChromeClient instance = + instanceManager.getInstanceWithWeakReference(instanceId)!; + if (instance.onShowFileChooser != null) { + return instance.onShowFileChooser!( + instanceManager.getInstanceWithWeakReference(webViewInstanceId)! + as WebView, + instanceManager.getInstanceWithWeakReference(paramsInstanceId)! + as FileChooserParams, + ); + } + + return Future>.value(const []); + } } /// Host api implementation for [WebStorage]. @@ -836,3 +876,32 @@ class WebStorageHostApiImpl extends WebStorageHostApi { return deleteAllData(instanceManager.getIdentifier(instance)!); } } + +/// Flutter api implementation for [FileChooserParams]. +class FileChooserParamsFlutterApiImpl extends FileChooserParamsFlutterApi { + /// Constructs a [FileChooserParamsFlutterApiImpl]. + FileChooserParamsFlutterApiImpl({InstanceManager? instanceManager}) + : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + + /// Maintains instances stored to communicate with java objects. + final InstanceManager instanceManager; + + @override + void create( + int instanceId, + bool isCaptureEnabled, + List acceptTypes, + FileChooserModeEnumData mode, + String? filenameHint, + ) { + instanceManager.addHostCreatedInstance( + FileChooserParams.detached( + isCaptureEnabled: isCaptureEnabled, + acceptTypes: acceptTypes.cast(), + mode: mode.value, + filenameHint: filenameHint, + ), + instanceId, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart index 2e32705a83ea..6bd3dc03746c 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_controller.dart @@ -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(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) +// ignore: unnecessary_import +import 'dart:async'; + // TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) // ignore: unnecessary_import import 'dart:typed_data'; @@ -11,10 +15,8 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -import 'android_navigation_delegate.dart'; import 'android_proxy.dart'; import 'android_webview.dart' as android_webview; -import 'android_webview.dart'; import 'instance_manager.dart'; import 'platform_views_service_proxy.dart'; import 'weak_reference_utils.dart'; @@ -76,6 +78,8 @@ class AndroidWebViewController extends PlatformWebViewController { _webView.settings.setUseWideViewPort(true); _webView.settings.setDisplayZoomControls(false); _webView.settings.setBuiltInZoomControls(true); + + _webView.setWebChromeClient(_webChromeClient); } AndroidWebViewControllerCreationParams get _androidWebViewParams => @@ -91,6 +95,32 @@ class AndroidWebViewController extends PlatformWebViewController { useHybridComposition: true, ); + late final android_webview.WebChromeClient _webChromeClient = + _androidWebViewParams.androidWebViewProxy.createAndroidWebChromeClient( + onProgressChanged: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (android_webview.WebView webView, int progress) { + if (weakReference.target?._currentNavigationDelegate?._onProgress != + null) { + weakReference + .target!._currentNavigationDelegate!._onProgress!(progress); + } + }; + }), + onShowFileChooser: withWeakReferenceTo(this, + (WeakReference weakReference) { + return (android_webview.WebView webView, + android_webview.FileChooserParams params) async { + if (weakReference.target?._onShowFileSelectorCallback != null) { + return weakReference.target!._onShowFileSelectorCallback!( + FileSelectorParams._fromFileChooserParams(params), + ); + } + return []; + }; + }), + ); + /// The native [android_webview.FlutterAssetManager] allows managing assets. late final android_webview.FlutterAssetManager _flutterAssetManager = _androidWebViewParams.androidWebViewProxy.createFlutterAssetManager(); @@ -98,10 +128,10 @@ class AndroidWebViewController extends PlatformWebViewController { final Map _javaScriptChannelParams = {}; - // The keeps a reference to the current NavigationDelegate so that the - // callback methods remain reachable. - // ignore: unused_field - late AndroidNavigationDelegate _currentNavigationDelegate; + AndroidNavigationDelegate? _currentNavigationDelegate; + + Future> Function(FileSelectorParams)? + _onShowFileSelectorCallback; /// Whether to enable the platform's webview content debugging tools. /// @@ -114,6 +144,16 @@ class AndroidWebViewController extends PlatformWebViewController { return webViewProxy.setWebContentsDebuggingEnabled(enabled); } + /// Identifier used to retrieve the underlying native `WKWebView`. + /// + /// This is typically used by other plugins to retrieve the native `WebView` + /// from an `InstanceManager`. + /// + /// See Java method `WebViewFlutterPlugin.getWebView`. + int get webViewIdentifier => + // ignore: invalid_use_of_visible_for_testing_member + android_webview.WebView.api.instanceManager.getIdentifier(_webView)!; + @override Future loadFile( String absoluteFilePath, @@ -175,11 +215,17 @@ class AndroidWebViewController extends PlatformWebViewController { case LoadRequestMethod.post: return _webView.postUrl( params.uri.toString(), params.body ?? Uint8List(0)); - default: - throw UnimplementedError( - 'This version of `AndroidWebViewController` currently has no implementation for HTTP method ${params.method.serialize()} in loadRequest.', - ); } + // The enum comes from a different package, which could get a new value at + // any time, so a fallback case is necessary. Since there is no reasonable + // default behavior, throw to alert the client that they need an updated + // version. This is deliberately outside the switch rather than a `default` + // so that the linter will flag the switch as needing an update. + // ignore: dead_code + throw UnimplementedError( + 'This version of `AndroidWebViewController` currently has no ' + 'implementation for HTTP method ${params.method.serialize()} in ' + 'loadRequest.'); } @override @@ -213,7 +259,6 @@ class AndroidWebViewController extends PlatformWebViewController { _currentNavigationDelegate = handler; handler.setOnLoadRequest(loadRequest); _webView.setWebViewClient(handler.androidWebViewClient); - _webView.setWebChromeClient(handler.androidWebChromeClient); _webView.setDownloadListener(handler.androidDownloadListener); } @@ -309,6 +354,79 @@ class AndroidWebViewController extends PlatformWebViewController { Future setMediaPlaybackRequiresUserGesture(bool require) { return _webView.settings.setMediaPlaybackRequiresUserGesture(require); } + + /// Sets the callback that is invoked when the client should show a file + /// selector. + Future setOnShowFileSelector( + Future> Function(FileSelectorParams params)? + onShowFileSelector, + ) { + _onShowFileSelectorCallback = onShowFileSelector; + return _webChromeClient.setSynchronousReturnValueForOnShowFileChooser( + onShowFileSelector != null, + ); + } +} + +/// Mode of how to select files for a file chooser. +enum FileSelectorMode { + /// Open single file and requires that the file exists before allowing the + /// user to pick it. + open, + + /// Similar to [open] but allows multiple files to be selected. + openMultiple, + + /// Allows picking a nonexistent file and saving it. + save, +} + +/// Parameters received when the `WebView` should show a file selector. +@immutable +class FileSelectorParams { + /// Constructs a [FileSelectorParams]. + const FileSelectorParams({ + required this.isCaptureEnabled, + required this.acceptTypes, + this.filenameHint, + required this.mode, + }); + + factory FileSelectorParams._fromFileChooserParams( + android_webview.FileChooserParams params, + ) { + final FileSelectorMode mode; + switch (params.mode) { + case android_webview.FileChooserMode.open: + mode = FileSelectorMode.open; + break; + case android_webview.FileChooserMode.openMultiple: + mode = FileSelectorMode.openMultiple; + break; + case android_webview.FileChooserMode.save: + mode = FileSelectorMode.save; + break; + } + + return FileSelectorParams( + isCaptureEnabled: params.isCaptureEnabled, + acceptTypes: params.acceptTypes, + mode: mode, + filenameHint: params.filenameHint, + ); + } + + /// Preference for a live media captured value (e.g. Camera, Microphone). + final bool isCaptureEnabled; + + /// A list of acceptable MIME types. + final List acceptTypes; + + /// The file name of a default selection if specified, or null. + final String? filenameHint; + + /// Mode of how to select files for a file selector. + final FileSelectorMode mode; } /// An implementation of [JavaScriptChannelParams] with the Android WebView API. @@ -325,7 +443,7 @@ class AndroidJavaScriptChannelParams extends JavaScriptChannelParams { }) : assert(name.isNotEmpty), _javaScriptChannel = webViewProxy.createJavaScriptChannel( name, - postMessage: withWeakRefenceTo( + postMessage: withWeakReferenceTo( onMessageReceived, (WeakReference weakReference) { return ( @@ -374,7 +492,8 @@ class AndroidWebViewWidgetCreationParams @visibleForTesting InstanceManager? instanceManager, @visibleForTesting this.platformViewsServiceProxy = const PlatformViewsServiceProxy(), - }) : instanceManager = instanceManager ?? JavaObject.globalInstanceManager; + }) : instanceManager = + instanceManager ?? android_webview.JavaObject.globalInstanceManager; /// Constructs a [WebKitWebViewWidgetCreationParams] using a /// [PlatformWebViewWidgetCreationParams]. @@ -488,3 +607,308 @@ class AndroidWebViewWidget extends PlatformWebViewWidget { } } } + +/// Signature for the `loadRequest` callback responsible for loading the [url] +/// after a navigation request has been approved. +typedef LoadRequestCallback = Future Function(LoadRequestParams params); + +/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. +@immutable +class AndroidWebResourceError extends WebResourceError { + /// Creates a new [AndroidWebResourceError]. + AndroidWebResourceError._({ + required super.errorCode, + required super.description, + super.isForMainFrame, + this.failingUrl, + }) : super( + errorType: _errorCodeToErrorType(errorCode), + ); + + /// Gets the URL for which the failing resource request was made. + final String? failingUrl; + + static WebResourceErrorType? _errorCodeToErrorType(int errorCode) { + switch (errorCode) { + case android_webview.WebViewClient.errorAuthentication: + return WebResourceErrorType.authentication; + case android_webview.WebViewClient.errorBadUrl: + return WebResourceErrorType.badUrl; + case android_webview.WebViewClient.errorConnect: + return WebResourceErrorType.connect; + case android_webview.WebViewClient.errorFailedSslHandshake: + return WebResourceErrorType.failedSslHandshake; + case android_webview.WebViewClient.errorFile: + return WebResourceErrorType.file; + case android_webview.WebViewClient.errorFileNotFound: + return WebResourceErrorType.fileNotFound; + case android_webview.WebViewClient.errorHostLookup: + return WebResourceErrorType.hostLookup; + case android_webview.WebViewClient.errorIO: + return WebResourceErrorType.io; + case android_webview.WebViewClient.errorProxyAuthentication: + return WebResourceErrorType.proxyAuthentication; + case android_webview.WebViewClient.errorRedirectLoop: + return WebResourceErrorType.redirectLoop; + case android_webview.WebViewClient.errorTimeout: + return WebResourceErrorType.timeout; + case android_webview.WebViewClient.errorTooManyRequests: + return WebResourceErrorType.tooManyRequests; + case android_webview.WebViewClient.errorUnknown: + return WebResourceErrorType.unknown; + case android_webview.WebViewClient.errorUnsafeResource: + return WebResourceErrorType.unsafeResource; + case android_webview.WebViewClient.errorUnsupportedAuthScheme: + return WebResourceErrorType.unsupportedAuthScheme; + case android_webview.WebViewClient.errorUnsupportedScheme: + return WebResourceErrorType.unsupportedScheme; + } + + throw ArgumentError( + 'Could not find a WebResourceErrorType for errorCode: $errorCode', + ); + } +} + +/// Object specifying creation parameters for creating a [AndroidNavigationDelegate]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformNavigationDelegateCreationParams] for +/// more information. +@immutable +class AndroidNavigationDelegateCreationParams + extends PlatformNavigationDelegateCreationParams { + /// Creates a new [AndroidNavigationDelegateCreationParams] instance. + const AndroidNavigationDelegateCreationParams._({ + @visibleForTesting this.androidWebViewProxy = const AndroidWebViewProxy(), + }) : super(); + + /// Creates a [AndroidNavigationDelegateCreationParams] instance based on [PlatformNavigationDelegateCreationParams]. + factory AndroidNavigationDelegateCreationParams.fromPlatformNavigationDelegateCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformNavigationDelegateCreationParams params, { + @visibleForTesting + AndroidWebViewProxy androidWebViewProxy = const AndroidWebViewProxy(), + }) { + return AndroidNavigationDelegateCreationParams._( + androidWebViewProxy: androidWebViewProxy, + ); + } + + /// Handles constructing objects and calling static methods for the Android WebView + /// native library. + @visibleForTesting + final AndroidWebViewProxy androidWebViewProxy; +} + +/// A place to register callback methods responsible to handle navigation events +/// triggered by the [android_webview.WebView]. +class AndroidNavigationDelegate extends PlatformNavigationDelegate { + /// Creates a new [AndroidNavigationDelegate]. + AndroidNavigationDelegate(PlatformNavigationDelegateCreationParams params) + : super.implementation(params is AndroidNavigationDelegateCreationParams + ? params + : AndroidNavigationDelegateCreationParams + .fromPlatformNavigationDelegateCreationParams(params)) { + final WeakReference weakThis = + WeakReference(this); + + _webViewClient = (this.params as AndroidNavigationDelegateCreationParams) + .androidWebViewProxy + .createAndroidWebViewClient( + onPageFinished: (android_webview.WebView webView, String url) { + if (weakThis.target?._onPageFinished != null) { + weakThis.target!._onPageFinished!(url); + } + }, + onPageStarted: (android_webview.WebView webView, String url) { + if (weakThis.target?._onPageStarted != null) { + weakThis.target!._onPageStarted!(url); + } + }, + onReceivedRequestError: ( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + android_webview.WebResourceError error, + ) { + if (weakThis.target?._onWebResourceError != null) { + weakThis.target!._onWebResourceError!(AndroidWebResourceError._( + errorCode: error.errorCode, + description: error.description, + failingUrl: request.url, + isForMainFrame: request.isForMainFrame, + )); + } + }, + onReceivedError: ( + android_webview.WebView webView, + int errorCode, + String description, + String failingUrl, + ) { + if (weakThis.target?._onWebResourceError != null) { + weakThis.target!._onWebResourceError!(AndroidWebResourceError._( + errorCode: errorCode, + description: description, + failingUrl: failingUrl, + isForMainFrame: true, + )); + } + }, + requestLoading: ( + android_webview.WebView webView, + android_webview.WebResourceRequest request, + ) { + if (weakThis.target != null) { + weakThis.target!._handleNavigation( + request.url, + headers: request.requestHeaders, + isForMainFrame: request.isForMainFrame, + ); + } + }, + urlLoading: ( + android_webview.WebView webView, + String url, + ) { + if (weakThis.target != null) { + weakThis.target!._handleNavigation(url, isForMainFrame: true); + } + }, + ); + + _downloadListener = (this.params as AndroidNavigationDelegateCreationParams) + .androidWebViewProxy + .createDownloadListener( + onDownloadStart: ( + String url, + String userAgent, + String contentDisposition, + String mimetype, + int contentLength, + ) { + if (weakThis.target != null) { + weakThis.target?._handleNavigation(url, isForMainFrame: true); + } + }, + ); + } + + AndroidNavigationDelegateCreationParams get _androidParams => + params as AndroidNavigationDelegateCreationParams; + + late final android_webview.WebChromeClient _webChromeClient = + _androidParams.androidWebViewProxy.createAndroidWebChromeClient(); + + /// Gets the native [android_webview.WebChromeClient] that is bridged by this [AndroidNavigationDelegate]. + /// + /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setWebChromeClient`. + @Deprecated( + 'This value is not used by `AndroidWebViewController` and has no effect on the `WebView`.', + ) + android_webview.WebChromeClient get androidWebChromeClient => + _webChromeClient; + + late final android_webview.WebViewClient _webViewClient; + + /// Gets the native [android_webview.WebViewClient] that is bridged by this [AndroidNavigationDelegate]. + /// + /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setWebViewClient`. + android_webview.WebViewClient get androidWebViewClient => _webViewClient; + + late final android_webview.DownloadListener _downloadListener; + + /// Gets the native [android_webview.DownloadListener] that is bridged by this [AndroidNavigationDelegate]. + /// + /// Used by the [AndroidWebViewController] to set the `android_webview.WebView.setDownloadListener`. + android_webview.DownloadListener get androidDownloadListener => + _downloadListener; + + PageEventCallback? _onPageFinished; + PageEventCallback? _onPageStarted; + ProgressCallback? _onProgress; + WebResourceErrorCallback? _onWebResourceError; + NavigationRequestCallback? _onNavigationRequest; + LoadRequestCallback? _onLoadRequest; + + void _handleNavigation( + String url, { + required bool isForMainFrame, + Map headers = const {}, + }) { + final LoadRequestCallback? onLoadRequest = _onLoadRequest; + final NavigationRequestCallback? onNavigationRequest = _onNavigationRequest; + + if (onNavigationRequest == null || onLoadRequest == null) { + return; + } + + final FutureOr returnValue = onNavigationRequest( + NavigationRequest( + url: url, + isMainFrame: isForMainFrame, + ), + ); + + if (returnValue is NavigationDecision && + returnValue == NavigationDecision.navigate) { + onLoadRequest(LoadRequestParams( + uri: Uri.parse(url), + headers: headers, + )); + } else if (returnValue is Future) { + returnValue.then((NavigationDecision shouldLoadUrl) { + if (shouldLoadUrl == NavigationDecision.navigate) { + onLoadRequest(LoadRequestParams( + uri: Uri.parse(url), + headers: headers, + )); + } + }); + } + } + + /// Invoked when loading the url after a navigation request is approved. + Future setOnLoadRequest( + LoadRequestCallback onLoadRequest, + ) async { + _onLoadRequest = onLoadRequest; + } + + @override + Future setOnNavigationRequest( + NavigationRequestCallback onNavigationRequest, + ) async { + _onNavigationRequest = onNavigationRequest; + _webViewClient.setSynchronousReturnValueForShouldOverrideUrlLoading(true); + } + + @override + Future setOnPageStarted( + PageEventCallback onPageStarted, + ) async { + _onPageStarted = onPageStarted; + } + + @override + Future setOnPageFinished( + PageEventCallback onPageFinished, + ) async { + _onPageFinished = onPageFinished; + } + + @override + Future setOnProgress( + ProgressCallback onProgress, + ) async { + _onProgress = onProgress; + } + + @override + Future setOnWebResourceError( + WebResourceErrorCallback onWebResourceError, + ) async { + _onWebResourceError = onWebResourceError; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_platform.dart b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_platform.dart index 24581ebd24dc..7997f69d7eba 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_platform.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/android_webview_platform.dart @@ -4,7 +4,6 @@ import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; -import 'android_navigation_delegate.dart'; import 'android_webview_controller.dart'; import 'android_webview_cookie_manager.dart'; diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart index fc1028e7af15..cd4ba820cf4c 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/legacy/webview_android_widget.dart @@ -134,7 +134,7 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { final Map _javaScriptChannels = {}; - late final android_webview.WebViewClient _webViewClient = withWeakRefenceTo( + late final android_webview.WebViewClient _webViewClient = withWeakReferenceTo( this, (WeakReference weakReference) { return webViewProxy.createWebViewClient( onPageStarted: (_, String url) { @@ -213,7 +213,7 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { @visibleForTesting late final android_webview.DownloadListener downloadListener = android_webview.DownloadListener( - onDownloadStart: withWeakRefenceTo( + onDownloadStart: withWeakReferenceTo( this, (WeakReference weakReference) { return ( @@ -236,7 +236,7 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { @visibleForTesting late final android_webview.WebChromeClient webChromeClient = android_webview.WebChromeClient( - onProgressChanged: withWeakRefenceTo( + onProgressChanged: withWeakReferenceTo( this, (WeakReference weakReference) { return (_, int progress) { @@ -320,11 +320,17 @@ class WebViewAndroidPlatformController extends WebViewPlatformController { case WebViewRequestMethod.post: return webView.postUrl( request.uri.toString(), request.body ?? Uint8List(0)); - default: - throw UnimplementedError( - 'This version of webview_android_widget currently has no implementation for HTTP method ${request.method.serialize()} in loadRequest.', - ); } + // The enum comes from a different package, which could get a new value at + // any time, so a fallback case is necessary. Since there is no reasonable + // default behavior, throw to alert the client that they need an updated + // version. This is deliberately outside the switch rather than a `default` + // so that the linter will flag the switch as needing an update. + // ignore: dead_code + throw UnimplementedError( + 'This version of webview_android_widget currently has no ' + 'implementation for HTTP method ${request.method.serialize()} in ' + 'loadRequest.'); } @override @@ -574,7 +580,7 @@ class WebViewAndroidJavaScriptChannel super.channelName, this.javascriptChannelRegistry, ) : super( - postMessage: withWeakRefenceTo( + postMessage: withWeakReferenceTo( javascriptChannelRegistry, (WeakReference weakReference) { return (String message) { diff --git a/packages/webview_flutter/webview_flutter_android/lib/src/weak_reference_utils.dart b/packages/webview_flutter/webview_flutter_android/lib/src/weak_reference_utils.dart index ad0c9ebf4f5c..fd3e3f0dc273 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/src/weak_reference_utils.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/src/weak_reference_utils.dart @@ -25,7 +25,7 @@ /// ), /// ); /// ``` -S withWeakRefenceTo( +S withWeakReferenceTo( T reference, S Function(WeakReference weakReference) onCreate, ) { diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_flutter_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_flutter_android.dart index faab71549995..95f835ed8a1d 100644 --- a/packages/webview_flutter/webview_flutter_android/lib/webview_flutter_android.dart +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_flutter_android.dart @@ -4,7 +4,6 @@ library webview_flutter_android; -export 'src/android_navigation_delegate.dart'; export 'src/android_webview_controller.dart'; export 'src/android_webview_cookie_manager.dart'; export 'src/android_webview_platform.dart'; diff --git a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart index d3adac8ee4c4..7f4d362c9273 100644 --- a/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart +++ b/packages/webview_flutter/webview_flutter_android/pigeons/android_webview.dart @@ -6,8 +6,8 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( - dartOut: 'lib/src/android_webview.pigeon.dart', - dartTestOut: 'test/test_android_webview.pigeon.dart', + dartOut: 'lib/src/android_webview.g.dart', + dartTestOut: 'test/test_android_webview.g.dart', dartOptions: DartOptions(copyrightHeader: [ 'Copyright 2013 The Flutter Authors. All rights reserved.', 'Use of this source code is governed by a BSD-style license that can be', @@ -26,6 +26,34 @@ import 'package:pigeon/pigeon.dart'; ), ), ) + +/// Mode of how to select files for a file chooser. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. +enum FileChooserMode { + /// Open single file and requires that the file exists before allowing the + /// user to pick it. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_OPEN. + open, + + /// Similar to [open] but allows multiple files to be selected. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_OPEN_MULTIPLE. + openMultiple, + + /// Allows picking a nonexistent file and saving it. + /// + /// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams#MODE_SAVE. + save, +} + +// TODO(bparrishMines): Enums need be wrapped in a data class because thay can't +// be used as primitive arguments. See https://github.com/flutter/flutter/issues/87307 +class FileChooserModeEnumData { + late FileChooserMode value; +} + class WebResourceRequestData { WebResourceRequestData( this.url, @@ -262,6 +290,11 @@ abstract class DownloadListenerFlutterApi { @HostApi(dartHostTestHandler: 'TestWebChromeClientHostApi') abstract class WebChromeClientHostApi { void create(int instanceId); + + void setSynchronousReturnValueForOnShowFileChooser( + int instanceId, + bool value, + ); } @HostApi(dartHostTestHandler: 'TestAssetManagerHostApi') @@ -274,6 +307,13 @@ abstract class FlutterAssetManagerHostApi { @FlutterApi() abstract class WebChromeClientFlutterApi { void onProgressChanged(int instanceId, int webViewInstanceId, int progress); + + @async + List onShowFileChooser( + int instanceId, + int webViewInstanceId, + int paramsInstanceId, + ); } @HostApi(dartHostTestHandler: 'TestWebStorageHostApi') @@ -282,3 +322,17 @@ abstract class WebStorageHostApi { void deleteAllData(int instanceId); } + +/// Handles callbacks methods for the native Java FileChooserParams class. +/// +/// See https://developer.android.com/reference/android/webkit/WebChromeClient.FileChooserParams. +@FlutterApi() +abstract class FileChooserParamsFlutterApi { + void create( + int instanceId, + bool isCaptureEnabled, + List acceptTypes, + FileChooserModeEnumData mode, + String? filenameHint, + ); +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 123a7c918870..ac8971006ba2 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.1.3 +version: 3.3.0 environment: sdk: ">=2.17.0 <3.0.0" @@ -29,4 +29,4 @@ dev_dependencies: flutter_test: sdk: flutter mockito: ^5.3.2 - pigeon: ^4.2.3 + pigeon: ^4.2.14 diff --git a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart index 26d4e686d389..dac7c69a84f3 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_navigation_delegate_test.dart @@ -346,22 +346,6 @@ void main() { isTrue); }); - test('onProgress', () { - final AndroidNavigationDelegate androidNavigationDelegate = - AndroidNavigationDelegate(_buildCreationParams()); - - late final int callbackProgress; - androidNavigationDelegate - .setOnProgress((int progress) => callbackProgress = progress); - - CapturingWebChromeClient.lastCreatedDelegate.onProgressChanged!( - android_webview.WebView.detached(), - 42, - ); - - expect(callbackProgress, 42); - }); - test( 'onLoadRequest from onDownloadStart should not be called when navigationRequestCallback is not specified', () { @@ -495,6 +479,7 @@ class CapturingWebViewClient extends android_webview.WebViewClient { class CapturingWebChromeClient extends android_webview.WebChromeClient { CapturingWebChromeClient({ super.onProgressChanged, + super.onShowFileChooser, }) : super.detached() { lastCreatedDelegate = this; } diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart index 0e7842ab467d..43bab384e0cc 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.dart @@ -14,11 +14,13 @@ import 'package:mockito/mockito.dart'; import 'package:webview_flutter_android/src/android_proxy.dart'; import 'package:webview_flutter_android/src/android_webview.dart' as android_webview; +import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; import 'package:webview_flutter_android/src/instance_manager.dart'; import 'package:webview_flutter_android/src/platform_views_service_proxy.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; import 'package:webview_flutter_platform_interface/src/webview_platform.dart'; +import 'android_navigation_delegate_test.dart'; import 'android_webview_controller_test.mocks.dart'; @GenerateNiceMocks(>[ @@ -44,7 +46,16 @@ void main() { AndroidWebViewController createControllerWithMocks({ android_webview.FlutterAssetManager? mockFlutterAssetManager, android_webview.JavaScriptChannel? mockJavaScriptChannel, - android_webview.WebChromeClient? mockWebChromeClient, + android_webview.WebChromeClient Function({ + void Function(android_webview.WebView webView, int progress)? + onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? + onShowFileChooser, + })? + createWebChromeClient, android_webview.WebView? mockWebView, android_webview.WebViewClient? mockWebViewClient, android_webview.WebStorage? mockWebStorage, @@ -57,10 +68,17 @@ void main() { AndroidWebViewControllerCreationParams( androidWebStorage: mockWebStorage ?? MockWebStorage(), androidWebViewProxy: AndroidWebViewProxy( - createAndroidWebChromeClient: ( - {void Function(android_webview.WebView, int)? - onProgressChanged}) => - mockWebChromeClient ?? MockWebChromeClient(), + createAndroidWebChromeClient: createWebChromeClient ?? + ({ + void Function(android_webview.WebView, int)? + onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? + onShowFileChooser, + }) => + MockWebChromeClient(), createAndroidWebView: ({required bool useHybridComposition}) => nonNullMockWebView, createAndroidWebViewClient: ({ @@ -486,10 +504,101 @@ void main() { await controller.setPlatformNavigationDelegate(mockNavigationDelegate); - verifyInOrder([ - mockWebView.setWebViewClient(mockWebViewClient), - mockWebView.setWebChromeClient(mockWebChromeClient), - ]); + verify(mockWebView.setWebViewClient(mockWebViewClient)); + verifyNever(mockWebView.setWebChromeClient(mockWebChromeClient)); + }); + + test('onProgress', () { + final AndroidNavigationDelegate androidNavigationDelegate = + AndroidNavigationDelegate( + AndroidNavigationDelegateCreationParams + .fromPlatformNavigationDelegateCreationParams( + const PlatformNavigationDelegateCreationParams(), + androidWebViewProxy: const AndroidWebViewProxy( + createAndroidWebViewClient: android_webview.WebViewClient.detached, + createAndroidWebChromeClient: + android_webview.WebChromeClient.detached, + createDownloadListener: android_webview.DownloadListener.detached, + ), + ), + ); + + late final int callbackProgress; + androidNavigationDelegate + .setOnProgress((int progress) => callbackProgress = progress); + + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: CapturingWebChromeClient.new, + ); + controller.setPlatformNavigationDelegate(androidNavigationDelegate); + + CapturingWebChromeClient.lastCreatedDelegate.onProgressChanged!( + android_webview.WebView.detached(), + 42, + ); + + expect(callbackProgress, 42); + }); + + test('onProgress does not cause LateInitializationError', () { + // ignore: unused_local_variable + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: CapturingWebChromeClient.new, + ); + + // Should not cause LateInitializationError + CapturingWebChromeClient.lastCreatedDelegate.onProgressChanged!( + android_webview.WebView.detached(), + 42, + ); + }); + + test('setOnShowFileSelector', () async { + late final Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + ) onShowFileChooserCallback; + final MockWebChromeClient mockWebChromeClient = MockWebChromeClient(); + final AndroidWebViewController controller = createControllerWithMocks( + createWebChromeClient: ({ + dynamic onProgressChanged, + Future> Function( + android_webview.WebView webView, + android_webview.FileChooserParams params, + )? + onShowFileChooser, + }) { + onShowFileChooserCallback = onShowFileChooser!; + return mockWebChromeClient; + }, + ); + + late final FileSelectorParams fileSelectorParams; + await controller.setOnShowFileSelector( + (FileSelectorParams params) async { + fileSelectorParams = params; + return []; + }, + ); + + verify( + mockWebChromeClient.setSynchronousReturnValueForOnShowFileChooser(true), + ); + + onShowFileChooserCallback( + android_webview.WebView.detached(), + android_webview.FileChooserParams.detached( + isCaptureEnabled: false, + acceptTypes: ['png'], + filenameHint: 'filenameHint', + mode: android_webview.FileChooserMode.open, + ), + ); + + expect(fileSelectorParams.isCaptureEnabled, isFalse); + expect(fileSelectorParams.acceptTypes, ['png']); + expect(fileSelectorParams.filenameHint, 'filenameHint'); + expect(fileSelectorParams.mode, FileSelectorMode.open); }); test('runJavaScript', () async { @@ -776,6 +885,29 @@ void main() { verify(mockSettings.setMediaPlaybackRequiresUserGesture(true)).called(1); }); + test('webViewIdentifier', () { + final MockWebView mockWebView = MockWebView(); + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + instanceManager.addHostCreatedInstance(mockWebView, 0); + + android_webview.WebView.api = WebViewHostApiImpl( + instanceManager: instanceManager, + ); + + final AndroidWebViewController controller = createControllerWithMocks( + mockWebView: mockWebView, + ); + + expect( + controller.webViewIdentifier, + 0, + ); + + android_webview.WebView.api = WebViewHostApiImpl(); + }); + group('AndroidWebViewWidget', () { testWidgets('Builds Android view using supplied parameters', (WidgetTester tester) async { diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart index 643f5ac964b1..01885caff54c 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_controller_test.mocks.dart @@ -4,20 +4,18 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i9; -import 'dart:typed_data' as _i15; +import 'dart:typed_data' as _i14; import 'dart:ui' as _i4; -import 'package:flutter/foundation.dart' as _i12; -import 'package:flutter/gestures.dart' as _i13; -import 'package:flutter/material.dart' as _i14; +import 'package:flutter/foundation.dart' as _i11; +import 'package:flutter/gestures.dart' as _i12; +import 'package:flutter/material.dart' as _i13; import 'package:flutter/services.dart' as _i7; import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_android/src/android_navigation_delegate.dart' - as _i8; -import 'package:webview_flutter_android/src/android_proxy.dart' as _i11; +import 'package:webview_flutter_android/src/android_proxy.dart' as _i10; import 'package:webview_flutter_android/src/android_webview.dart' as _i2; import 'package:webview_flutter_android/src/android_webview_controller.dart' - as _i10; + as _i8; import 'package:webview_flutter_android/src/instance_manager.dart' as _i5; import 'package:webview_flutter_android/src/platform_views_service_proxy.dart' as _i6; @@ -349,7 +347,7 @@ class MockAndroidNavigationDelegate extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockAndroidWebViewController extends _i1.Mock - implements _i10.AndroidWebViewController { + implements _i8.AndroidWebViewController { @override _i3.PlatformWebViewControllerCreationParams get params => (super.noSuchMethod( Invocation.getter(#params), @@ -649,13 +647,25 @@ class MockAndroidWebViewController extends _i1.Mock returnValue: _i9.Future.value(), returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); + @override + _i9.Future setOnShowFileSelector( + _i9.Future> Function(_i8.FileSelectorParams)? + onShowFileSelectorCallback) => + (super.noSuchMethod( + Invocation.method( + #setOnShowFileSelector, + [onShowFileSelectorCallback], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); } /// A class which mocks [AndroidWebViewProxy]. /// /// See the documentation for Mockito's code generation for more information. class MockAndroidWebViewProxy extends _i1.Mock - implements _i11.AndroidWebViewProxy { + implements _i10.AndroidWebViewProxy { @override _i2.WebView Function({required bool useHybridComposition}) get createAndroidWebView => (super.noSuchMethod( @@ -672,40 +682,63 @@ class MockAndroidWebViewProxy extends _i1.Mock ), ) as _i2.WebView Function({required bool useHybridComposition})); @override - _i2.WebChromeClient Function( - {void Function( - _i2.WebView, - int, - )? - onProgressChanged}) get createAndroidWebChromeClient => - (super.noSuchMethod( + _i2.WebChromeClient Function({ + void Function( + _i2.WebView, + int, + )? + onProgressChanged, + _i9.Future> Function( + _i2.WebView, + _i2.FileChooserParams, + )? + onShowFileChooser, + }) get createAndroidWebChromeClient => (super.noSuchMethod( Invocation.getter(#createAndroidWebChromeClient), - returnValue: ( - {void Function( - _i2.WebView, - int, - )? - onProgressChanged}) => + returnValue: ({ + void Function( + _i2.WebView, + int, + )? + onProgressChanged, + _i9.Future> Function( + _i2.WebView, + _i2.FileChooserParams, + )? + onShowFileChooser, + }) => _FakeWebChromeClient_0( this, Invocation.getter(#createAndroidWebChromeClient), ), - returnValueForMissingStub: ( - {void Function( - _i2.WebView, - int, - )? - onProgressChanged}) => + returnValueForMissingStub: ({ + void Function( + _i2.WebView, + int, + )? + onProgressChanged, + _i9.Future> Function( + _i2.WebView, + _i2.FileChooserParams, + )? + onShowFileChooser, + }) => _FakeWebChromeClient_0( this, Invocation.getter(#createAndroidWebChromeClient), ), - ) as _i2.WebChromeClient Function( - {void Function( - _i2.WebView, - int, - )? - onProgressChanged})); + ) as _i2.WebChromeClient Function({ + void Function( + _i2.WebView, + int, + )? + onProgressChanged, + _i9.Future> Function( + _i2.WebView, + _i2.FileChooserParams, + )? + onShowFileChooser, + })); @override _i2.WebViewClient Function({ void Function( @@ -958,7 +991,7 @@ class MockAndroidWebViewProxy extends _i1.Mock /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable class MockAndroidWebViewWidgetCreationParams extends _i1.Mock - implements _i10.AndroidWebViewWidgetCreationParams { + implements _i8.AndroidWebViewWidgetCreationParams { @override _i5.InstanceManager get instanceManager => (super.noSuchMethod( Invocation.getter(#instanceManager), @@ -1009,13 +1042,13 @@ class MockAndroidWebViewWidgetCreationParams extends _i1.Mock returnValueForMissingStub: _i4.TextDirection.rtl, ) as _i4.TextDirection); @override - Set<_i12.Factory<_i13.OneSequenceGestureRecognizer>> get gestureRecognizers => + Set<_i11.Factory<_i12.OneSequenceGestureRecognizer>> get gestureRecognizers => (super.noSuchMethod( Invocation.getter(#gestureRecognizers), - returnValue: <_i12.Factory<_i13.OneSequenceGestureRecognizer>>{}, + returnValue: <_i11.Factory<_i12.OneSequenceGestureRecognizer>>{}, returnValueForMissingStub: < - _i12.Factory<_i13.OneSequenceGestureRecognizer>>{}, - ) as Set<_i12.Factory<_i13.OneSequenceGestureRecognizer>>); + _i11.Factory<_i12.OneSequenceGestureRecognizer>>{}, + ) as Set<_i11.Factory<_i12.OneSequenceGestureRecognizer>>); } /// A class which mocks [ExpensiveAndroidViewController]. @@ -1162,7 +1195,7 @@ class MockExpensiveAndroidViewController extends _i1.Mock returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); @override - _i9.Future dispatchPointerEvent(_i14.PointerEvent? event) => + _i9.Future dispatchPointerEvent(_i13.PointerEvent? event) => (super.noSuchMethod( Invocation.method( #dispatchPointerEvent, @@ -1514,7 +1547,7 @@ class MockSurfaceAndroidViewController extends _i1.Mock returnValueForMissingStub: _i9.Future.value(), ) as _i9.Future); @override - _i9.Future dispatchPointerEvent(_i14.PointerEvent? event) => + _i9.Future dispatchPointerEvent(_i13.PointerEvent? event) => (super.noSuchMethod( Invocation.method( #dispatchPointerEvent, @@ -1548,6 +1581,16 @@ class MockSurfaceAndroidViewController extends _i1.Mock /// See the documentation for Mockito's code generation for more information. class MockWebChromeClient extends _i1.Mock implements _i2.WebChromeClient { @override + _i9.Future setSynchronousReturnValueForOnShowFileChooser(bool? value) => + (super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForOnShowFileChooser, + [value], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + @override _i2.WebChromeClient copy() => (super.noSuchMethod( Invocation.method( #copy, @@ -1793,7 +1836,7 @@ class MockWebView extends _i1.Mock implements _i2.WebView { @override _i9.Future postUrl( String? url, - _i15.Uint8List? data, + _i14.Uint8List? data, ) => (super.noSuchMethod( Invocation.method( diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart index 4e972fe7b98d..236d87da44eb 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.dart @@ -6,12 +6,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:webview_flutter_android/src/android_webview.dart'; -import 'package:webview_flutter_android/src/android_webview.pigeon.dart'; +import 'package:webview_flutter_android/src/android_webview.g.dart'; import 'package:webview_flutter_android/src/android_webview_api_impls.dart'; import 'package:webview_flutter_android/src/instance_manager.dart'; import 'android_webview_test.mocks.dart'; -import 'test_android_webview.pigeon.dart'; +import 'test_android_webview.g.dart'; @GenerateMocks([ CookieManagerHostApi, @@ -831,10 +831,120 @@ void main() { expect(result, containsAllInOrder([mockWebView, 76])); }); + test('onShowFileChooser', () async { + late final List result; + when(mockWebChromeClient.onShowFileChooser).thenReturn( + (WebView webView, FileChooserParams params) { + result = [webView, params]; + return Future>.value(['fileOne', 'fileTwo']); + }, + ); + + final FileChooserParams params = FileChooserParams.detached( + isCaptureEnabled: false, + acceptTypes: [], + filenameHint: 'filenameHint', + mode: FileChooserMode.open, + ); + + instanceManager.addHostCreatedInstance(params, 3); + + await expectLater( + flutterApi.onShowFileChooser( + mockWebChromeClientInstanceId, + mockWebViewInstanceId, + 3, + ), + completion(['fileOne', 'fileTwo']), + ); + expect(result[0], mockWebView); + expect(result[1], params); + }); + + test('setSynchronousReturnValueForOnShowFileChooser', () { + final MockTestWebChromeClientHostApi mockHostApi = + MockTestWebChromeClientHostApi(); + TestWebChromeClientHostApi.setup(mockHostApi); + + WebChromeClient.api = + WebChromeClientHostApiImpl(instanceManager: instanceManager); + + final WebChromeClient webChromeClient = WebChromeClient.detached(); + instanceManager.addHostCreatedInstance(webChromeClient, 2); + + webChromeClient.setSynchronousReturnValueForOnShowFileChooser(false); + + verify( + mockHostApi.setSynchronousReturnValueForOnShowFileChooser(2, false), + ); + }); + + test( + 'setSynchronousReturnValueForOnShowFileChooser throws StateError when onShowFileChooser is null', + () { + final MockTestWebChromeClientHostApi mockHostApi = + MockTestWebChromeClientHostApi(); + TestWebChromeClientHostApi.setup(mockHostApi); + + WebChromeClient.api = + WebChromeClientHostApiImpl(instanceManager: instanceManager); + + final WebChromeClient clientWithNullCallback = + WebChromeClient.detached(); + instanceManager.addHostCreatedInstance(clientWithNullCallback, 2); + + expect( + () => clientWithNullCallback + .setSynchronousReturnValueForOnShowFileChooser(true), + throwsStateError, + ); + + final WebChromeClient clientWithNonnullCallback = + WebChromeClient.detached( + onShowFileChooser: (_, __) async => [], + ); + instanceManager.addHostCreatedInstance(clientWithNonnullCallback, 3); + + clientWithNonnullCallback + .setSynchronousReturnValueForOnShowFileChooser(true); + + verify( + mockHostApi.setSynchronousReturnValueForOnShowFileChooser(3, true), + ); + }); + test('copy', () { expect(WebChromeClient.detached().copy(), isA()); }); }); + + group('FileChooserParams', () { + test('FlutterApi create', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final FileChooserParamsFlutterApiImpl flutterApi = + FileChooserParamsFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create( + 0, + false, + const ['my', 'list'], + FileChooserModeEnumData(value: FileChooserMode.openMultiple), + 'filenameHint', + ); + + final FileChooserParams instance = instanceManager + .getInstanceWithWeakReference(0)! as FileChooserParams; + expect(instance.isCaptureEnabled, false); + expect(instance.acceptTypes, const ['my', 'list']); + expect(instance.mode, FileChooserMode.openMultiple); + expect(instance.filenameHint, 'filenameHint'); + }); + }); }); group('CookieManager', () { diff --git a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart index 81cdc9ef545f..0b5afbaf5b13 100644 --- a/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/android_webview_test.mocks.dart @@ -9,9 +9,9 @@ import 'dart:ui' as _i4; import 'package:mockito/mockito.dart' as _i1; import 'package:webview_flutter_android/src/android_webview.dart' as _i2; -import 'package:webview_flutter_android/src/android_webview.pigeon.dart' as _i3; +import 'package:webview_flutter_android/src/android_webview.g.dart' as _i3; -import 'test_android_webview.pigeon.dart' as _i6; +import 'test_android_webview.g.dart' as _i6; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -304,6 +304,21 @@ class MockTestWebChromeClientHostApi extends _i1.Mock ), returnValueForMissingStub: null, ); + @override + void setSynchronousReturnValueForOnShowFileChooser( + int? instanceId, + bool? value, + ) => + super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForOnShowFileChooser, + [ + instanceId, + value, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestWebSettingsHostApi]. @@ -952,6 +967,16 @@ class MockWebChromeClient extends _i1.Mock implements _i2.WebChromeClient { _i1.throwOnMissingStub(this); } + @override + _i5.Future setSynchronousReturnValueForOnShowFileChooser(bool? value) => + (super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForOnShowFileChooser, + [value], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i2.WebChromeClient copy() => (super.noSuchMethod( Invocation.method( diff --git a/packages/webview_flutter/webview_flutter_android/test/legacy/surface_android_test.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/surface_android_test.dart index 196e7cc27093..d022ab282c92 100644 --- a/packages/webview_flutter/webview_flutter_android/test/legacy/surface_android_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/surface_android_test.dart @@ -17,7 +17,10 @@ void main() { late List log; setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler( + SystemChannels.platform_views, (MethodCall call) async { log.add(call); if (call.method == 'resize') { @@ -29,12 +32,15 @@ void main() { 'height': arguments['height'], }; } + return null; }, ); }); tearDownAll(() { - SystemChannels.platform_views.setMockMethodCallHandler(null); + _ambiguate(TestDefaultBinaryMessengerBinding.instance)! + .defaultBinaryMessenger + .setMockMethodCallHandler(SystemChannels.platform_views, null); }); setUp(() { @@ -116,3 +122,9 @@ class TestWebViewPlatformCallbacksHandler @override void onWebResourceError(WebResourceError error) {} } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +T? _ambiguate(T? value) => value; diff --git a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.dart index 22c161afa180..44cc18510909 100644 --- a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.dart @@ -16,7 +16,7 @@ import 'package:webview_flutter_android/src/legacy/webview_android_widget.dart'; import 'package:webview_flutter_platform_interface/src/webview_flutter_platform_interface_legacy.dart'; import '../android_webview_test.mocks.dart' show MockTestWebViewHostApi; -import '../test_android_webview.pigeon.dart'; +import '../test_android_webview.g.dart'; import 'webview_android_widget_test.mocks.dart'; @GenerateMocks([ diff --git a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart index 1f16c29aa953..03489ce5c1e0 100644 --- a/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_android/test/legacy/webview_android_widget_test.mocks.dart @@ -763,6 +763,16 @@ class MockWebChromeClient extends _i1.Mock implements _i2.WebChromeClient { _i1.throwOnMissingStub(this); } + @override + _i5.Future setSynchronousReturnValueForOnShowFileChooser(bool? value) => + (super.noSuchMethod( + Invocation.method( + #setSynchronousReturnValueForOnShowFileChooser, + [value], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i2.WebChromeClient copy() => (super.noSuchMethod( Invocation.method( diff --git a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart similarity index 94% rename from packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart rename to packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart index bcfb453e85c4..56ba79a66622 100644 --- a/packages/webview_flutter/webview_flutter_android/test/test_android_webview.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_android/test/test_android_webview.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v4.2.3), do not edit directly. +// Autogenerated from Pigeon (v4.2.14), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import // ignore_for_file: avoid_relative_lib_imports @@ -11,7 +11,7 @@ import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_android/src/android_webview.pigeon.dart'; +import 'package:webview_flutter_android/src/android_webview.g.dart'; /// Handles methods calls to the native Java Object class. /// @@ -22,6 +22,7 @@ abstract class TestJavaObjectHostApi { static const MessageCodec codec = StandardMessageCodec(); void dispose(int identifier); + static void setup(TestJavaObjectHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -39,7 +40,7 @@ abstract class TestJavaObjectHostApi { assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.JavaObjectHostApi.dispose was null, expected non-null int.'); api.dispose(arg_identifier!); - return {}; + return []; }); } } @@ -74,33 +75,59 @@ abstract class TestWebViewHostApi { static const MessageCodec codec = _TestWebViewHostApiCodec(); void create(int instanceId, bool useHybridComposition); + void loadData( int instanceId, String data, String? mimeType, String? encoding); + void loadDataWithBaseUrl(int instanceId, String? baseUrl, String data, String? mimeType, String? encoding, String? historyUrl); + void loadUrl(int instanceId, String url, Map headers); + void postUrl(int instanceId, String url, Uint8List data); + String? getUrl(int instanceId); + bool canGoBack(int instanceId); + bool canGoForward(int instanceId); + void goBack(int instanceId); + void goForward(int instanceId); + void reload(int instanceId); + void clearCache(int instanceId, bool includeDiskFiles); + Future evaluateJavascript(int instanceId, String javascriptString); + String? getTitle(int instanceId); + void scrollTo(int instanceId, int x, int y); + void scrollBy(int instanceId, int x, int y); + int getScrollX(int instanceId); + int getScrollY(int instanceId); + WebViewPoint getScrollPosition(int instanceId); + void setWebContentsDebuggingEnabled(bool enabled); + void setWebViewClient(int instanceId, int webViewClientInstanceId); + void addJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); + void removeJavaScriptChannel(int instanceId, int javaScriptChannelInstanceId); + void setDownloadListener(int instanceId, int? listenerInstanceId); + void setWebChromeClient(int instanceId, int? clientInstanceId); + void setBackgroundColor(int instanceId, int color); + static void setup(TestWebViewHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -121,7 +148,7 @@ abstract class TestWebViewHostApi { assert(arg_useHybridComposition != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.create was null, expected non-null bool.'); api.create(arg_instanceId!, arg_useHybridComposition!); - return {}; + return []; }); } } @@ -145,7 +172,7 @@ abstract class TestWebViewHostApi { final String? arg_mimeType = (args[2] as String?); final String? arg_encoding = (args[3] as String?); api.loadData(arg_instanceId!, arg_data!, arg_mimeType, arg_encoding); - return {}; + return []; }); } } @@ -172,7 +199,7 @@ abstract class TestWebViewHostApi { final String? arg_historyUrl = (args[5] as String?); api.loadDataWithBaseUrl(arg_instanceId!, arg_baseUrl, arg_data!, arg_mimeType, arg_encoding, arg_historyUrl); - return {}; + return []; }); } } @@ -198,7 +225,7 @@ abstract class TestWebViewHostApi { assert(arg_headers != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.loadUrl was null, expected non-null Map.'); api.loadUrl(arg_instanceId!, arg_url!, arg_headers!); - return {}; + return []; }); } } @@ -223,7 +250,7 @@ abstract class TestWebViewHostApi { assert(arg_data != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.postUrl was null, expected non-null Uint8List.'); api.postUrl(arg_instanceId!, arg_url!, arg_data!); - return {}; + return []; }); } } @@ -242,7 +269,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getUrl was null, expected non-null int.'); final String? output = api.getUrl(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -261,7 +288,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.canGoBack was null, expected non-null int.'); final bool output = api.canGoBack(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -280,7 +307,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.canGoForward was null, expected non-null int.'); final bool output = api.canGoForward(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -299,7 +326,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.goBack was null, expected non-null int.'); api.goBack(arg_instanceId!); - return {}; + return []; }); } } @@ -318,7 +345,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.goForward was null, expected non-null int.'); api.goForward(arg_instanceId!); - return {}; + return []; }); } } @@ -337,7 +364,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.reload was null, expected non-null int.'); api.reload(arg_instanceId!); - return {}; + return []; }); } } @@ -359,7 +386,7 @@ abstract class TestWebViewHostApi { assert(arg_includeDiskFiles != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.clearCache was null, expected non-null bool.'); api.clearCache(arg_instanceId!, arg_includeDiskFiles!); - return {}; + return []; }); } } @@ -382,7 +409,7 @@ abstract class TestWebViewHostApi { 'Argument for dev.flutter.pigeon.WebViewHostApi.evaluateJavascript was null, expected non-null String.'); final String? output = await api.evaluateJavascript( arg_instanceId!, arg_javascriptString!); - return {'result': output}; + return [output]; }); } } @@ -401,7 +428,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getTitle was null, expected non-null int.'); final String? output = api.getTitle(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -426,7 +453,7 @@ abstract class TestWebViewHostApi { assert(arg_y != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollTo was null, expected non-null int.'); api.scrollTo(arg_instanceId!, arg_x!, arg_y!); - return {}; + return []; }); } } @@ -451,7 +478,7 @@ abstract class TestWebViewHostApi { assert(arg_y != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.scrollBy was null, expected non-null int.'); api.scrollBy(arg_instanceId!, arg_x!, arg_y!); - return {}; + return []; }); } } @@ -470,7 +497,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollX was null, expected non-null int.'); final int output = api.getScrollX(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -489,7 +516,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollY was null, expected non-null int.'); final int output = api.getScrollY(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -508,7 +535,7 @@ abstract class TestWebViewHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.getScrollPosition was null, expected non-null int.'); final WebViewPoint output = api.getScrollPosition(arg_instanceId!); - return {'result': output}; + return [output]; }); } } @@ -528,7 +555,7 @@ abstract class TestWebViewHostApi { assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebContentsDebuggingEnabled was null, expected non-null bool.'); api.setWebContentsDebuggingEnabled(arg_enabled!); - return {}; + return []; }); } } @@ -550,7 +577,7 @@ abstract class TestWebViewHostApi { assert(arg_webViewClientInstanceId != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebViewClient was null, expected non-null int.'); api.setWebViewClient(arg_instanceId!, arg_webViewClientInstanceId!); - return {}; + return []; }); } } @@ -573,7 +600,7 @@ abstract class TestWebViewHostApi { 'Argument for dev.flutter.pigeon.WebViewHostApi.addJavaScriptChannel was null, expected non-null int.'); api.addJavaScriptChannel( arg_instanceId!, arg_javaScriptChannelInstanceId!); - return {}; + return []; }); } } @@ -596,7 +623,7 @@ abstract class TestWebViewHostApi { 'Argument for dev.flutter.pigeon.WebViewHostApi.removeJavaScriptChannel was null, expected non-null int.'); api.removeJavaScriptChannel( arg_instanceId!, arg_javaScriptChannelInstanceId!); - return {}; + return []; }); } } @@ -616,7 +643,7 @@ abstract class TestWebViewHostApi { 'Argument for dev.flutter.pigeon.WebViewHostApi.setDownloadListener was null, expected non-null int.'); final int? arg_listenerInstanceId = (args[1] as int?); api.setDownloadListener(arg_instanceId!, arg_listenerInstanceId); - return {}; + return []; }); } } @@ -636,7 +663,7 @@ abstract class TestWebViewHostApi { 'Argument for dev.flutter.pigeon.WebViewHostApi.setWebChromeClient was null, expected non-null int.'); final int? arg_clientInstanceId = (args[1] as int?); api.setWebChromeClient(arg_instanceId!, arg_clientInstanceId); - return {}; + return []; }); } } @@ -658,7 +685,7 @@ abstract class TestWebViewHostApi { assert(arg_color != null, 'Argument for dev.flutter.pigeon.WebViewHostApi.setBackgroundColor was null, expected non-null int.'); api.setBackgroundColor(arg_instanceId!, arg_color!); - return {}; + return []; }); } } @@ -669,18 +696,31 @@ abstract class TestWebSettingsHostApi { static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId, int webViewInstanceId); + void setDomStorageEnabled(int instanceId, bool flag); + void setJavaScriptCanOpenWindowsAutomatically(int instanceId, bool flag); + void setSupportMultipleWindows(int instanceId, bool support); + void setJavaScriptEnabled(int instanceId, bool flag); + void setUserAgentString(int instanceId, String? userAgentString); + void setMediaPlaybackRequiresUserGesture(int instanceId, bool require); + void setSupportZoom(int instanceId, bool support); + void setLoadWithOverviewMode(int instanceId, bool overview); + void setUseWideViewPort(int instanceId, bool use); + void setDisplayZoomControls(int instanceId, bool enabled); + void setBuiltInZoomControls(int instanceId, bool enabled); + void setAllowFileAccess(int instanceId, bool enabled); + static void setup(TestWebSettingsHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -701,7 +741,7 @@ abstract class TestWebSettingsHostApi { assert(arg_webViewInstanceId != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!, arg_webViewInstanceId!); - return {}; + return []; }); } } @@ -723,7 +763,7 @@ abstract class TestWebSettingsHostApi { assert(arg_flag != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDomStorageEnabled was null, expected non-null bool.'); api.setDomStorageEnabled(arg_instanceId!, arg_flag!); - return {}; + return []; }); } } @@ -747,7 +787,7 @@ abstract class TestWebSettingsHostApi { 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptCanOpenWindowsAutomatically was null, expected non-null bool.'); api.setJavaScriptCanOpenWindowsAutomatically( arg_instanceId!, arg_flag!); - return {}; + return []; }); } } @@ -770,7 +810,7 @@ abstract class TestWebSettingsHostApi { assert(arg_support != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportMultipleWindows was null, expected non-null bool.'); api.setSupportMultipleWindows(arg_instanceId!, arg_support!); - return {}; + return []; }); } } @@ -792,7 +832,7 @@ abstract class TestWebSettingsHostApi { assert(arg_flag != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setJavaScriptEnabled was null, expected non-null bool.'); api.setJavaScriptEnabled(arg_instanceId!, arg_flag!); - return {}; + return []; }); } } @@ -812,7 +852,7 @@ abstract class TestWebSettingsHostApi { 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUserAgentString was null, expected non-null int.'); final String? arg_userAgentString = (args[1] as String?); api.setUserAgentString(arg_instanceId!, arg_userAgentString); - return {}; + return []; }); } } @@ -836,7 +876,7 @@ abstract class TestWebSettingsHostApi { 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setMediaPlaybackRequiresUserGesture was null, expected non-null bool.'); api.setMediaPlaybackRequiresUserGesture( arg_instanceId!, arg_require!); - return {}; + return []; }); } } @@ -858,7 +898,7 @@ abstract class TestWebSettingsHostApi { assert(arg_support != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setSupportZoom was null, expected non-null bool.'); api.setSupportZoom(arg_instanceId!, arg_support!); - return {}; + return []; }); } } @@ -881,7 +921,7 @@ abstract class TestWebSettingsHostApi { assert(arg_overview != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setLoadWithOverviewMode was null, expected non-null bool.'); api.setLoadWithOverviewMode(arg_instanceId!, arg_overview!); - return {}; + return []; }); } } @@ -903,7 +943,7 @@ abstract class TestWebSettingsHostApi { assert(arg_use != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setUseWideViewPort was null, expected non-null bool.'); api.setUseWideViewPort(arg_instanceId!, arg_use!); - return {}; + return []; }); } } @@ -925,7 +965,7 @@ abstract class TestWebSettingsHostApi { assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setDisplayZoomControls was null, expected non-null bool.'); api.setDisplayZoomControls(arg_instanceId!, arg_enabled!); - return {}; + return []; }); } } @@ -947,7 +987,7 @@ abstract class TestWebSettingsHostApi { assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setBuiltInZoomControls was null, expected non-null bool.'); api.setBuiltInZoomControls(arg_instanceId!, arg_enabled!); - return {}; + return []; }); } } @@ -969,7 +1009,7 @@ abstract class TestWebSettingsHostApi { assert(arg_enabled != null, 'Argument for dev.flutter.pigeon.WebSettingsHostApi.setAllowFileAccess was null, expected non-null bool.'); api.setAllowFileAccess(arg_instanceId!, arg_enabled!); - return {}; + return []; }); } } @@ -980,6 +1020,7 @@ abstract class TestJavaScriptChannelHostApi { static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId, String channelName); + static void setup(TestJavaScriptChannelHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1000,7 +1041,7 @@ abstract class TestJavaScriptChannelHostApi { assert(arg_channelName != null, 'Argument for dev.flutter.pigeon.JavaScriptChannelHostApi.create was null, expected non-null String.'); api.create(arg_instanceId!, arg_channelName!); - return {}; + return []; }); } } @@ -1011,8 +1052,10 @@ abstract class TestWebViewClientHostApi { static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId); + void setSynchronousReturnValueForShouldOverrideUrlLoading( int instanceId, bool value); + static void setup(TestWebViewClientHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1030,7 +1073,7 @@ abstract class TestWebViewClientHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebViewClientHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!); - return {}; + return []; }); } } @@ -1054,7 +1097,7 @@ abstract class TestWebViewClientHostApi { 'Argument for dev.flutter.pigeon.WebViewClientHostApi.setSynchronousReturnValueForShouldOverrideUrlLoading was null, expected non-null bool.'); api.setSynchronousReturnValueForShouldOverrideUrlLoading( arg_instanceId!, arg_value!); - return {}; + return []; }); } } @@ -1065,6 +1108,7 @@ abstract class TestDownloadListenerHostApi { static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId); + static void setup(TestDownloadListenerHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1082,7 +1126,7 @@ abstract class TestDownloadListenerHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.DownloadListenerHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!); - return {}; + return []; }); } } @@ -1093,6 +1137,10 @@ abstract class TestWebChromeClientHostApi { static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId); + + void setSynchronousReturnValueForOnShowFileChooser( + int instanceId, bool value); + static void setup(TestWebChromeClientHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1110,7 +1158,31 @@ abstract class TestWebChromeClientHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!); - return {}; + return []; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.WebChromeClientHostApi.setSynchronousReturnValueForOnShowFileChooser', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.setSynchronousReturnValueForOnShowFileChooser was null.'); + final List args = (message as List?)!; + final int? arg_instanceId = (args[0] as int?); + assert(arg_instanceId != null, + 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.setSynchronousReturnValueForOnShowFileChooser was null, expected non-null int.'); + final bool? arg_value = (args[1] as bool?); + assert(arg_value != null, + 'Argument for dev.flutter.pigeon.WebChromeClientHostApi.setSynchronousReturnValueForOnShowFileChooser was null, expected non-null bool.'); + api.setSynchronousReturnValueForOnShowFileChooser( + arg_instanceId!, arg_value!); + return []; }); } } @@ -1121,7 +1193,9 @@ abstract class TestAssetManagerHostApi { static const MessageCodec codec = StandardMessageCodec(); List list(String path); + String getAssetFilePathByName(String name); + static void setup(TestAssetManagerHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1139,7 +1213,7 @@ abstract class TestAssetManagerHostApi { assert(arg_path != null, 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.list was null, expected non-null String.'); final List output = api.list(arg_path!); - return {'result': output}; + return [output]; }); } } @@ -1159,7 +1233,7 @@ abstract class TestAssetManagerHostApi { assert(arg_name != null, 'Argument for dev.flutter.pigeon.FlutterAssetManagerHostApi.getAssetFilePathByName was null, expected non-null String.'); final String output = api.getAssetFilePathByName(arg_name!); - return {'result': output}; + return [output]; }); } } @@ -1170,7 +1244,9 @@ abstract class TestWebStorageHostApi { static const MessageCodec codec = StandardMessageCodec(); void create(int instanceId); + void deleteAllData(int instanceId); + static void setup(TestWebStorageHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1188,7 +1264,7 @@ abstract class TestWebStorageHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebStorageHostApi.create was null, expected non-null int.'); api.create(arg_instanceId!); - return {}; + return []; }); } } @@ -1207,7 +1283,7 @@ abstract class TestWebStorageHostApi { assert(arg_instanceId != null, 'Argument for dev.flutter.pigeon.WebStorageHostApi.deleteAllData was null, expected non-null int.'); api.deleteAllData(arg_instanceId!); - return {}; + return []; }); } } diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md index 19a7950e45ab..5c33fdbcea59 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Updates minimum Flutter version to 3.0. + ## 2.0.1 * Improves error message when a platform interface class is used before `WebViewPlatform.instance` has been set. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml index 865307245079..627b6098c302 100644 --- a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -8,7 +8,7 @@ version: 2.0.1 environment: sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md index 19d7a1c48181..3ada124fe7ce 100644 --- a/packages/webview_flutter/webview_flutter_web/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_web/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.2.2 + +* Updates `WebWebViewController.loadRequest` to only set the src of the iFrame + when `LoadRequestParams.headers` and `LoadRequestParams.body` are empty and is + using the HTTP GET request method. [#118573](https://github.com/flutter/flutter/issues/118573). +* Parses the `content-type` header of XHR responses to extract the correct + MIME-type and charset. [#118090](https://github.com/flutter/flutter/issues/118090). +* Sets `width` and `height` of widget the way the Engine wants, to remove distracting + warnings from the development console. +* Updates minimum Flutter version to 3.0. + ## 0.2.1 * Adds auto registration of the `WebViewPlatform` implementation. diff --git a/packages/webview_flutter/webview_flutter_web/README.md b/packages/webview_flutter/webview_flutter_web/README.md index 51a0223696d0..03bb6a89052e 100644 --- a/packages/webview_flutter/webview_flutter_web/README.md +++ b/packages/webview_flutter/webview_flutter_web/README.md @@ -21,3 +21,19 @@ yet, so it currently requires extra setup to use: Once the step above is complete, the APIs from `webview_flutter` listed above can be used as normal on web. + +## Tests + +Tests are contained in the `test` directory. You can run all tests from the root +of the package with the following command: + +```bash +$ flutter test --platform chrome +``` + +This package uses `package:mockito` in some tests. Mock files can be updated +from the root of the package like so: + +```bash +$ flutter pub run build_runner build --delete-conflicting-outputs +``` diff --git a/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart index 1736d47d39c8..f71d2d3c2bac 100644 --- a/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_web/example/integration_test/webview_flutter_test.dart @@ -5,6 +5,10 @@ import 'dart:html' as html; import 'dart:io'; +// FIX (dit): Remove these integration tests, or make them run. They currently never fail. +// (They won't run because they use `dart:io`. If you remove all `dart:io` bits from +// this file, they start failing with `fail()`, for example.) + import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml index 782817eb7100..4685135acdf1 100644 --- a/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_web/example/pubspec.yaml @@ -4,6 +4,7 @@ publish_to: none environment: sdk: ">=2.14.0 <3.0.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/webview_flutter/webview_flutter_web/example/run_test.sh b/packages/webview_flutter/webview_flutter_web/example/run_test.sh new file mode 100755 index 000000000000..aa52974f310e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/example/run_test.sh @@ -0,0 +1,22 @@ +#!/usr/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if pgrep -lf chromedriver > /dev/null; then + echo "chromedriver is running." + + 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.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.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/webview_flutter/webview_flutter_web/lib/src/content_type.dart b/packages/webview_flutter/webview_flutter_web/lib/src/content_type.dart new file mode 100644 index 000000000000..0aa18ce2318a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/lib/src/content_type.dart @@ -0,0 +1,48 @@ +// 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. + +/// Class to represent a content-type header value. +class ContentType { + /// Creates a [ContentType] instance by parsing a "content-type" response [header]. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type + /// See: https://httpwg.org/specs/rfc9110.html#media.type + ContentType.parse(String header) { + final Iterable chunks = + header.split(';').map((String e) => e.trim().toLowerCase()); + + for (final String chunk in chunks) { + if (!chunk.contains('=')) { + _mimeType = chunk; + } else { + final List bits = + chunk.split('=').map((String e) => e.trim()).toList(); + assert(bits.length == 2); + switch (bits[0]) { + case 'charset': + _charset = bits[1]; + break; + case 'boundary': + _boundary = bits[1]; + break; + default: + throw StateError('Unable to parse "$chunk" in content-type.'); + } + } + } + } + + String? _mimeType; + String? _charset; + String? _boundary; + + /// The MIME-type of the resource or the data. + String? get mimeType => _mimeType; + + /// The character encoding standard. + String? get charset => _charset; + + /// The separation boundary for multipart entities. + String? get boundary => _boundary; +} diff --git a/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart b/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart index 7ef72257999f..52f93f911e40 100644 --- a/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_web/lib/src/web_webview_controller.dart @@ -3,11 +3,12 @@ // found in the LICENSE file. import 'dart:convert'; -import 'dart:html'; +import 'dart:html' as html; import 'package:flutter/cupertino.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'content_type.dart'; import 'http_request_factory.dart'; import 'shims/dart_ui.dart' as ui; @@ -37,10 +38,10 @@ class WebWebViewControllerCreationParams /// The underlying element used as the WebView. @visibleForTesting - final IFrameElement iFrame = IFrameElement() + final html.IFrameElement iFrame = html.IFrameElement() ..id = 'webView${_nextIFrameId++}' - ..width = '100%' - ..height = '100%' + ..style.width = '100%' + ..style.height = '100%' ..style.border = 'none'; } @@ -72,20 +73,37 @@ class WebWebViewController extends PlatformWebViewController { throw ArgumentError( 'LoadRequestParams#uri is required to have a scheme.'); } - final HttpRequest httpReq = + + if (params.headers.isEmpty && + (params.body == null || params.body!.isEmpty) && + params.method == LoadRequestMethod.get) { + // ignore: unsafe_html + _webWebViewParams.iFrame.src = params.uri.toString(); + } else { + await _updateIFrameFromXhr(params); + } + } + + /// Performs an AJAX request defined by [params]. + Future _updateIFrameFromXhr(LoadRequestParams params) async { + final html.HttpRequest httpReq = await _webWebViewParams.httpRequestFactory.request( params.uri.toString(), method: params.method.serialize(), requestHeaders: params.headers, sendData: params.body, ); - final String contentType = + + final String header = httpReq.getResponseHeader('content-type') ?? 'text/html'; + final ContentType contentType = ContentType.parse(header); + final Encoding encoding = Encoding.getByName(contentType.charset) ?? utf8; + // ignore: unsafe_html _webWebViewParams.iFrame.src = Uri.dataFromString( httpReq.responseText ?? '', - mimeType: contentType, - encoding: utf8, + mimeType: contentType.mimeType, + encoding: encoding, ).toString(); } } diff --git a/packages/webview_flutter/webview_flutter_web/pubspec.yaml b/packages/webview_flutter/webview_flutter_web/pubspec.yaml index 99ff3d95f269..f3ea67d68dad 100644 --- a/packages/webview_flutter/webview_flutter_web/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_web/pubspec.yaml @@ -2,11 +2,11 @@ name: webview_flutter_web description: A Flutter plugin that provides a WebView widget on web. repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 0.2.1 +version: 0.2.2 environment: sdk: ">=2.14.0 <3.0.0" - flutter: ">=2.10.0" + flutter: ">=3.0.0" flutter: plugin: diff --git a/packages/webview_flutter/webview_flutter_web/test/content_type_test.dart b/packages/webview_flutter/webview_flutter_web/test/content_type_test.dart new file mode 100644 index 000000000000..936eeae4f571 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_web/test/content_type_test.dart @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_web/src/content_type.dart'; + +void main() { + group('ContentType.parse', () { + test('basic content-type (lowers case)', () { + final ContentType contentType = ContentType.parse('text/pLaIn'); + + expect(contentType.mimeType, 'text/plain'); + expect(contentType.boundary, isNull); + expect(contentType.charset, isNull); + }); + + test('with charset', () { + final ContentType contentType = + ContentType.parse('text/pLaIn; charset=utf-8'); + + expect(contentType.mimeType, 'text/plain'); + expect(contentType.boundary, isNull); + expect(contentType.charset, 'utf-8'); + }); + + test('with boundary', () { + final ContentType contentType = + ContentType.parse('text/pLaIn; boundary=---xyz'); + + expect(contentType.mimeType, 'text/plain'); + expect(contentType.boundary, '---xyz'); + expect(contentType.charset, isNull); + }); + + test('with charset and boundary', () { + final ContentType contentType = + ContentType.parse('text/pLaIn; charset=utf-8; boundary=---xyz'); + + expect(contentType.mimeType, 'text/plain'); + expect(contentType.boundary, '---xyz'); + expect(contentType.charset, 'utf-8'); + }); + + test('with boundary and charset', () { + final ContentType contentType = + ContentType.parse('text/pLaIn; boundary=---xyz; charset=utf-8'); + + expect(contentType.mimeType, 'text/plain'); + expect(contentType.boundary, '---xyz'); + expect(contentType.charset, 'utf-8'); + }); + + test('with a bunch of whitespace, boundary and charset', () { + final ContentType contentType = ContentType.parse( + ' text/pLaIn ; boundary=---xyz; charset=utf-8 '); + + expect(contentType.mimeType, 'text/plain'); + expect(contentType.boundary, '---xyz'); + expect(contentType.charset, 'utf-8'); + }); + + test('empty string', () { + final ContentType contentType = ContentType.parse(''); + + expect(contentType.mimeType, ''); + expect(contentType.boundary, isNull); + expect(contentType.charset, isNull); + }); + + test('unknown parameter (throws)', () { + expect(() { + ContentType.parse('text/pLaIn; wrong=utf-8'); + }, throwsStateError); + }); + }); +} diff --git a/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.dart index 6a8f73798107..0a995cbb67e0 100644 --- a/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_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. +import 'dart:convert'; import 'dart:html'; // TODO(a14n): remove this import once Flutter 3.1 or later reaches stable (including flutter/flutter#104231) // ignore: unnecessary_import @@ -17,9 +18,9 @@ import 'package:webview_flutter_web/webview_flutter_web.dart'; import 'web_webview_controller_test.mocks.dart'; -@GenerateMocks([ - HttpRequest, - HttpRequestFactory, +@GenerateMocks([], customMocks: >[ + MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), ]) void main() { WidgetsFlutterBinding.ensureInitialized(); @@ -31,8 +32,8 @@ void main() { WebWebViewControllerCreationParams(); expect(params.iFrame.id, contains('webView')); - expect(params.iFrame.width, '100%'); - expect(params.iFrame.height, '100%'); + expect(params.iFrame.style.width, '100%'); + expect(params.iFrame.style.height, '100%'); expect(params.iFrame.style.border, 'none'); }); }); @@ -62,7 +63,7 @@ void main() { }); group('loadRequest', () { - test('loadRequest throws ArgumentError on missing scheme', () async { + test('throws ArgumentError on missing scheme', () async { final WebWebViewController controller = WebWebViewController(WebWebViewControllerCreationParams()); @@ -73,8 +74,33 @@ void main() { throwsA(const TypeMatcher())); }); - test('loadRequest makes request and loads response into iframe', - () async { + test('skips XHR for simple GETs (no headers, no data)', () async { + final MockHttpRequestFactory mockHttpRequestFactory = + MockHttpRequestFactory(); + final WebWebViewController controller = + WebWebViewController(WebWebViewControllerCreationParams( + httpRequestFactory: mockHttpRequestFactory, + )); + + when(mockHttpRequestFactory.request( + any, + method: anyNamed('method'), + requestHeaders: anyNamed('requestHeaders'), + sendData: anyNamed('sendData'), + )).thenThrow( + StateError('The `request` method should not have been called.')); + + await controller.loadRequest(LoadRequestParams( + uri: Uri.parse('https://flutter.dev'), + )); + + expect( + (controller.params as WebWebViewControllerCreationParams).iFrame.src, + 'https://flutter.dev/', + ); + }); + + test('makes request and loads response into iframe', () async { final MockHttpRequestFactory mockHttpRequestFactory = MockHttpRequestFactory(); final WebWebViewController controller = @@ -114,7 +140,41 @@ void main() { ); }); - test('loadRequest escapes "#" correctly', () async { + test('parses content-type response header correctly', () async { + final MockHttpRequestFactory mockHttpRequestFactory = + MockHttpRequestFactory(); + final WebWebViewController controller = + WebWebViewController(WebWebViewControllerCreationParams( + httpRequestFactory: mockHttpRequestFactory, + )); + + final Encoding iso = Encoding.getByName('latin1')!; + + final MockHttpRequest mockHttpRequest = MockHttpRequest(); + when(mockHttpRequest.responseText) + .thenReturn(String.fromCharCodes(iso.encode('España'))); + when(mockHttpRequest.getResponseHeader('content-type')) + .thenReturn('Text/HTmL; charset=latin1'); + + when(mockHttpRequestFactory.request( + any, + method: anyNamed('method'), + requestHeaders: anyNamed('requestHeaders'), + sendData: anyNamed('sendData'), + )).thenAnswer((_) => Future.value(mockHttpRequest)); + + await controller.loadRequest(LoadRequestParams( + uri: Uri.parse('https://flutter.dev'), + method: LoadRequestMethod.post, + )); + + expect( + (controller.params as WebWebViewControllerCreationParams).iFrame.src, + 'data:text/html;charset=iso-8859-1,Espa%F1a', + ); + }); + + test('escapes "#" correctly', () async { final MockHttpRequestFactory mockHttpRequestFactory = MockHttpRequestFactory(); final WebWebViewController controller = diff --git a/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.mocks.dart b/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.mocks.dart index f74359aac431..5cb259a3f01a 100644 --- a/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_web/test/web_webview_controller_test.mocks.dart @@ -55,24 +55,23 @@ class _FakeHttpRequest_2 extends _i1.SmartFake implements _i2.HttpRequest { /// /// See the documentation for Mockito's code generation for more information. class MockHttpRequest extends _i1.Mock implements _i2.HttpRequest { - MockHttpRequest() { - _i1.throwOnMissingStub(this); - } - @override Map get responseHeaders => (super.noSuchMethod( Invocation.getter(#responseHeaders), returnValue: {}, + returnValueForMissingStub: {}, ) as Map); @override int get readyState => (super.noSuchMethod( Invocation.getter(#readyState), returnValue: 0, + returnValueForMissingStub: 0, ) as int); @override String get responseType => (super.noSuchMethod( Invocation.getter(#responseType), returnValue: '', + returnValueForMissingStub: '', ) as String); @override set responseType(String? value) => super.noSuchMethod( @@ -97,6 +96,10 @@ class MockHttpRequest extends _i1.Mock implements _i2.HttpRequest { this, Invocation.getter(#upload), ), + returnValueForMissingStub: _FakeHttpRequestUpload_0( + this, + Invocation.getter(#upload), + ), ) as _i2.HttpRequestUpload); @override set withCredentials(bool? value) => super.noSuchMethod( @@ -110,41 +113,49 @@ class MockHttpRequest extends _i1.Mock implements _i2.HttpRequest { _i3.Stream<_i2.Event> get onReadyStateChange => (super.noSuchMethod( Invocation.getter(#onReadyStateChange), returnValue: _i3.Stream<_i2.Event>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.Event>.empty(), ) as _i3.Stream<_i2.Event>); @override _i3.Stream<_i2.ProgressEvent> get onAbort => (super.noSuchMethod( Invocation.getter(#onAbort), returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), ) as _i3.Stream<_i2.ProgressEvent>); @override _i3.Stream<_i2.ProgressEvent> get onError => (super.noSuchMethod( Invocation.getter(#onError), returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), ) as _i3.Stream<_i2.ProgressEvent>); @override _i3.Stream<_i2.ProgressEvent> get onLoad => (super.noSuchMethod( Invocation.getter(#onLoad), returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), ) as _i3.Stream<_i2.ProgressEvent>); @override _i3.Stream<_i2.ProgressEvent> get onLoadEnd => (super.noSuchMethod( Invocation.getter(#onLoadEnd), returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), ) as _i3.Stream<_i2.ProgressEvent>); @override _i3.Stream<_i2.ProgressEvent> get onLoadStart => (super.noSuchMethod( Invocation.getter(#onLoadStart), returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), ) as _i3.Stream<_i2.ProgressEvent>); @override _i3.Stream<_i2.ProgressEvent> get onProgress => (super.noSuchMethod( Invocation.getter(#onProgress), returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), ) as _i3.Stream<_i2.ProgressEvent>); @override _i3.Stream<_i2.ProgressEvent> get onTimeout => (super.noSuchMethod( Invocation.getter(#onTimeout), returnValue: _i3.Stream<_i2.ProgressEvent>.empty(), + returnValueForMissingStub: _i3.Stream<_i2.ProgressEvent>.empty(), ) as _i3.Stream<_i2.ProgressEvent>); @override _i2.Events get on => (super.noSuchMethod( @@ -153,6 +164,10 @@ class MockHttpRequest extends _i1.Mock implements _i2.HttpRequest { this, Invocation.getter(#on), ), + returnValueForMissingStub: _FakeEvents_1( + this, + Invocation.getter(#on), + ), ) as _i2.Events); @override void open( @@ -192,13 +207,16 @@ class MockHttpRequest extends _i1.Mock implements _i2.HttpRequest { [], ), returnValue: '', + returnValueForMissingStub: '', ) as String); @override - String? getResponseHeader(String? name) => - (super.noSuchMethod(Invocation.method( - #getResponseHeader, - [name], - )) as String?); + String? getResponseHeader(String? name) => (super.noSuchMethod( + Invocation.method( + #getResponseHeader, + [name], + ), + returnValueForMissingStub: null, + ) as String?); @override void overrideMimeType(String? mime) => super.noSuchMethod( Invocation.method( @@ -271,6 +289,7 @@ class MockHttpRequest extends _i1.Mock implements _i2.HttpRequest { [event], ), returnValue: false, + returnValueForMissingStub: false, ) as bool); } @@ -279,10 +298,6 @@ class MockHttpRequest extends _i1.Mock implements _i2.HttpRequest { /// See the documentation for Mockito's code generation for more information. class MockHttpRequestFactory extends _i1.Mock implements _i4.HttpRequestFactory { - MockHttpRequestFactory() { - _i1.throwOnMissingStub(this); - } - @override _i3.Future<_i2.HttpRequest> request( String? url, { @@ -324,5 +339,22 @@ class MockHttpRequestFactory extends _i1.Mock }, ), )), + returnValueForMissingStub: + _i3.Future<_i2.HttpRequest>.value(_FakeHttpRequest_2( + this, + Invocation.method( + #request, + [url], + { + #method: method, + #withCredentials: withCredentials, + #responseType: responseType, + #mimeType: mimeType, + #requestHeaders: requestHeaders, + #sendData: sendData, + #onProgress: onProgress, + }, + ), + )), ) as _i3.Future<_i2.HttpRequest>); } diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index b4ad2a16b425..d0c5a726b5f7 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,19 @@ +## 3.1.0 + +* Adds support to access native `WKWebView`. + +## 3.0.5 + +* Renames Pigeon output files. + +## 3.0.4 + +* Fixes bug that prevented the web view from being garbage collected. + +## 3.0.3 + +* Updates example code for `use_build_context_synchronously` lint. + ## 3.0.2 * Updates code for stricter lint checks. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/README.md b/packages/webview_flutter/webview_flutter_wkwebview/README.md index 79359636e742..a393a71d2248 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/README.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/README.md @@ -7,6 +7,24 @@ The Apple WKWebView implementation of [`webview_flutter`][1]. This package is [endorsed][2], which means you can simply use `webview_flutter` normally. This package will be automatically included in your app when you do. +### External Native API + +The plugin also provides a native API accessible by the native code of iOS applications or packages. +This API follows the convention of breaking changes of the Dart API, which means that any changes to +the class that are not backwards compatible will only be made with a major version change of the +plugin. Native code other than this external API does not follow breaking change conventions, so +app or plugin clients should not use any other native APIs. + +The API can be accessed by importing the native plugin `webview_flutter_wkwebview`: + +Objective-C: + +```objectivec +@import webview_flutter_wkwebview; +``` + +Then you will have access to the native class `FWFWebViewFlutterWKWebViewExternalAPI`. + ## Contributing This package uses [pigeon][3] to generate the communication layer between Flutter and the host diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart index 946f27b5df83..16411b8140a5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart @@ -21,6 +21,7 @@ import 'package:integration_test/integration_test.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; import 'package:webview_flutter_wkwebview/src/common/weak_reference_utils.dart'; +import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; Future main() async { @@ -47,7 +48,7 @@ Future main() async { final String headersUrl = '$prefixUrl/headers'; testWidgets( - 'withWeakRefenceTo allows encapsulating class to be garbage collected', + 'withWeakReferenceTo allows encapsulating class to be garbage collected', (WidgetTester tester) async { final Completer gcCompleter = Completer(); final InstanceManager instanceManager = InstanceManager( @@ -68,21 +69,102 @@ Future main() async { expect(gcIdentifier, 0); }, timeout: const Timeout(Duration(seconds: 10))); + testWidgets( + 'WKWebView is released by garbage collection', + (WidgetTester tester) async { + final Completer webViewGCCompleter = Completer(); + + late final InstanceManager instanceManager; + instanceManager = + InstanceManager(onWeakReferenceRemoved: (int identifier) { + final Copyable instance = + instanceManager.getInstanceWithWeakReference(identifier)!; + if (instance is WKWebView && !webViewGCCompleter.isCompleted) { + webViewGCCompleter.complete(); + } + }); + + await tester.pumpWidget( + Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + WebKitWebViewWidgetCreationParams( + instanceManager: instanceManager, + controller: PlatformWebViewController( + WebKitWebViewControllerCreationParams( + instanceManager: instanceManager, + ), + ), + ), + ).build(context); + }, + ), + ); + await tester.pumpAndSettle(); + + await tester.pumpWidget(Container()); + + // Force garbage collection. + await IntegrationTestWidgetsFlutterBinding.instance + .watchPerformance(() async { + await tester.pumpAndSettle(); + }); + + await expectLater(webViewGCCompleter.future, completes); + }, + timeout: const Timeout(Duration(seconds: 10)), + ); + testWidgets('loadRequest', (WidgetTester tester) async { + final Completer pageFinished = Completer(); + final PlatformWebViewController controller = PlatformWebViewController( const PlatformWebViewControllerCreationParams(), - ); - controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + ) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageFinished.complete()), + ) + ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageFinished.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); }); testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async { + final Completer pageFinished = Completer(); + final PlatformWebViewController controller = PlatformWebViewController( const PlatformWebViewControllerCreationParams(), - ); - controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + ) + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setPlatformNavigationDelegate( + PlatformNavigationDelegate( + const PlatformNavigationDelegateCreationParams(), + )..setOnPageFinished((_) => pageFinished.complete()), + ) + ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageFinished.future; await expectLater( controller.runJavaScriptReturningResult('1 + 1'), @@ -113,6 +195,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoads.stream.firstWhere((String url) => url == headersUrl); final String content = await controller.runJavaScriptReturningResult( @@ -147,6 +237,14 @@ Future main() async { 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageFinished.future; await controller.runJavaScript('Echo.postMessage("hello");'); @@ -184,6 +282,14 @@ Future main() async { ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setUserAgent('Custom_User_Agent1'); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + final String customUserAgent2 = await _getUserAgent(controller); expect(customUserAgent2, 'Custom_User_Agent1'); }); @@ -250,6 +356,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; bool isPaused = @@ -274,6 +388,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; isPaused = @@ -447,6 +569,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; bool isPaused = @@ -471,6 +601,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; isPaused = @@ -509,6 +647,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; // On at least iOS, it does not appear to be guaranteed that the native @@ -565,6 +711,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; await tester.pumpAndSettle(const Duration(seconds: 3)); @@ -599,8 +753,7 @@ Future main() async { '${base64Encode(const Utf8Encoder().convert(blankPage))}'; testWidgets('can allow requests', (WidgetTester tester) async { - final StreamController pageLoads = - StreamController.broadcast(); + Completer pageLoaded = Completer(); final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), @@ -610,7 +763,7 @@ Future main() async { WebKitNavigationDelegate( const WebKitNavigationDelegateCreationParams(), ) - ..setOnPageFinished((String url) => pageLoads.add(url)) + ..setOnPageFinished((_) => pageLoaded.complete()) ..setOnNavigationRequest((NavigationRequest navigationRequest) { return (navigationRequest.url.contains('youtube.com')) ? NavigationDecision.prevent @@ -621,10 +774,20 @@ Future main() async { LoadRequestParams(uri: Uri.parse(blankPageEncoded)), ); - await pageLoads.stream.first; // Wait for initial page load. + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); await controller.runJavaScript('location.href = "$secondaryUrl"'); + await pageLoaded.future; - await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); }); @@ -633,7 +796,7 @@ Future main() async { final Completer errorCompleter = Completer(); - PlatformWebViewController( + final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), ) ..setJavaScriptMode(JavaScriptMode.unrestricted) @@ -648,6 +811,14 @@ Future main() async { LoadRequestParams(uri: Uri.parse('https://www.notawebsite..com')), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + final WebResourceError error = await errorCompleter.future; expect(error, isNotNull); @@ -660,7 +831,7 @@ Future main() async { Completer(); final Completer pageFinishCompleter = Completer(); - PlatformWebViewController( + final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), ) ..setJavaScriptMode(JavaScriptMode.unrestricted) @@ -681,6 +852,14 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; }); @@ -706,7 +885,7 @@ Future main() async { Completer(); final Completer pageFinishCompleter = Completer(); - PlatformWebViewController( + final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), ) ..setJavaScriptMode(JavaScriptMode.unrestricted) @@ -727,14 +906,21 @@ Future main() async { ), ); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + expect(errorCompleter.future, doesNotComplete); await pageFinishCompleter.future; }, ); testWidgets('can block requests', (WidgetTester tester) async { - final StreamController pageLoads = - StreamController.broadcast(); + Completer pageLoaded = Completer(); final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), @@ -744,7 +930,7 @@ Future main() async { WebKitNavigationDelegate( const WebKitNavigationDelegateCreationParams(), ) - ..setOnPageFinished((String url) => pageLoads.add(url)) + ..setOnPageFinished((_) => pageLoaded.complete()) ..setOnNavigationRequest((NavigationRequest navigationRequest) { return (navigationRequest.url.contains('youtube.com')) ? NavigationDecision.prevent @@ -753,22 +939,31 @@ Future main() async { ) ..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded))); - await pageLoads.stream.first; // Wait for initial page load. + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); await controller .runJavaScript('location.href = "https://www.youtube.com/"'); // There should never be any second page load, since our new URL is // blocked. Still wait for a potential page change for some time in order // to give the test a chance to fail. - await pageLoads.stream.first + await pageLoaded.future .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); final String? currentUrl = await controller.currentUrl(); expect(currentUrl, isNot(contains('youtube.com'))); }); testWidgets('supports asynchronous decisions', (WidgetTester tester) async { - final StreamController pageLoads = - StreamController.broadcast(); + Completer pageLoaded = Completer(); final PlatformWebViewController controller = PlatformWebViewController( WebKitWebViewControllerCreationParams(), @@ -778,7 +973,7 @@ Future main() async { WebKitNavigationDelegate( const WebKitNavigationDelegateCreationParams(), ) - ..setOnPageFinished((String url) => pageLoads.add(url)) + ..setOnPageFinished((_) => pageLoaded.complete()) ..setOnNavigationRequest( (NavigationRequest navigationRequest) async { NavigationDecision decision = NavigationDecision.prevent; @@ -790,10 +985,20 @@ Future main() async { ) ..loadRequest(LoadRequestParams(uri: Uri.parse(blankPageEncoded))); - await pageLoads.stream.first; // Wait for initial page load. + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + + await pageLoaded.future; // Wait for initial page load. + + pageLoaded = Completer(); await controller.runJavaScript('location.href = "$secondaryUrl"'); - await pageLoads.stream.first; // Wait for second page to load. + await pageLoaded.future; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); expect(currentUrl, secondaryUrl); }); @@ -807,6 +1012,14 @@ Future main() async { ..setAllowsBackForwardNavigationGestures(true) ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); }); @@ -824,6 +1037,15 @@ Future main() async { )..setOnPageFinished((_) => pageLoaded.complete())); await controller.runJavaScript('window.open("$primaryUrl", "_blank")'); + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); @@ -843,6 +1065,14 @@ Future main() async { )..setOnPageFinished((_) => pageLoaded.complete())) ..loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))); + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + return PlatformWebViewWidget( + PlatformWebViewWidgetCreationParams(controller: controller), + ).build(context); + }, + )); + expect(controller.currentUrl(), completion(primaryUrl)); await pageLoaded.future; pageLoaded = Completer(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj index 1efee8f844ef..9e1038d08279 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,12 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; 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 */; }; + 8F4FF949299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */; }; 8FA6A87928062CD000A4B183 /* FWFInstanceManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */; }; 8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */; }; 8FB79B55281B24F600C101D3 /* FWFDataConvertersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */; }; @@ -76,6 +77,7 @@ 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 = ""; }; + 8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewFlutterWKWebViewExternalAPITests.m; sourceTree = ""; }; 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFInstanceManagerTests.m; sourceTree = ""; }; 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFWebViewHostApiTests.m; sourceTree = ""; }; 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FWFDataConvertersTests.m; sourceTree = ""; }; @@ -145,6 +147,7 @@ isa = PBXGroup; children = ( 68BDCAED23C3F7CB00D9C032 /* Info.plist */, + 8F4FF948299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m */, 8FA6A87828062CD000A4B183 /* FWFInstanceManagerTests.m */, 8FB79B5228134C3100C101D3 /* FWFWebViewHostApiTests.m */, 8FB79B54281B24F600C101D3 /* FWFDataConvertersTests.m */, @@ -379,10 +382,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -415,6 +420,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -463,6 +469,7 @@ 8FB79B5328134C3100C101D3 /* FWFWebViewHostApiTests.m in Sources */, 8FB79B73282096B500C101D3 /* FWFScriptMessageHandlerHostApiTests.m in Sources */, 8FB79B7928209D1300C101D3 /* FWFUserContentControllerHostApiTests.m in Sources */, + 8F4FF949299ADC2D000A6586 /* FWFWebViewFlutterWKWebViewExternalAPITests.m in Sources */, 8FB79B6B28204EE500C101D3 /* FWFWebsiteDataStoreHostApiTests.m in Sources */, 8FB79B8F2820BAB300C101D3 /* FWFScrollViewHostApiTests.m in Sources */, 8FB79B912820BAC700C101D3 /* FWFUIViewHostApiTests.m in Sources */, @@ -533,7 +540,11 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; @@ -547,7 +558,11 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; @@ -672,7 +687,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -695,7 +713,10 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); LIBRARY_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -711,7 +732,11 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -724,7 +749,11 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = RunnerUITests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewFlutterWKWebViewExternalAPITests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewFlutterWKWebViewExternalAPITests.m new file mode 100644 index 000000000000..1452edeaa647 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FWFWebViewFlutterWKWebViewExternalAPITests.m @@ -0,0 +1,28 @@ +// 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 +#import + +@import webview_flutter_wkwebview; + +@interface FWFWebViewFlutterWKWebViewExternalAPITests : XCTestCase +@end + +@implementation FWFWebViewFlutterWKWebViewExternalAPITests +- (void)testWebViewForIdentifier { + WKWebView *webView = [[WKWebView alloc] init]; + FWFInstanceManager *instanceManager = [[FWFInstanceManager alloc] init]; + [instanceManager addDartCreatedInstance:webView withIdentifier:0]; + + id mockPluginRegistry = OCMProtocolMock(@protocol(FlutterPluginRegistry)); + OCMStub([mockPluginRegistry valuePublishedByPlugin:@"FLTWebViewFlutterPlugin"]) + .andReturn(instanceManager); + + XCTAssertEqualObjects( + [FWFWebViewFlutterWKWebViewExternalAPI webViewForIdentifier:0 + withPluginRegistry:mockPluginRegistry], + webView); +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart index 84aced1b75e8..aef7ece0c2e3 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -165,9 +165,11 @@ Page resource error: return FloatingActionButton( onPressed: () async { final String? url = await _controller.currentUrl(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Favorited $url')), - ); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + } }, child: const Icon(Icons.favorite), ); @@ -320,25 +322,29 @@ class SampleMenu extends StatelessWidget { Future _onListCookies(BuildContext context) async { final String cookies = await webViewController .runJavaScriptReturningResult('document.cookie') as String; - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Cookies:'), - _getCookieList(cookies), - ], - ), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } } Future _onAddToCache(BuildContext context) async { await webViewController.runJavaScript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";', ); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Added a test entry to cache.'), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } } Future _onListCache() { @@ -351,9 +357,11 @@ class SampleMenu extends StatelessWidget { Future _onClearCache(BuildContext context) async { await webViewController.clearCache(); await webViewController.clearLocalStorage(); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Cache cleared.'), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Cache cleared.'), + )); + } } Future _onClearCookies(BuildContext context) async { @@ -362,9 +370,11 @@ class SampleMenu extends StatelessWidget { if (!hadCookies) { message = 'There are no cookies.'; } - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(message), - )); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } } Future _onNavigationDelegateExample() { @@ -463,10 +473,11 @@ class NavigationControls extends StatelessWidget { if (await webViewController.canGoBack()) { await webViewController.goBack(); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No back history item')), - ); - return; + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No back history item')), + ); + } } }, ), @@ -476,10 +487,11 @@ class NavigationControls extends StatelessWidget { if (await webViewController.canGoForward()) { await webViewController.goForward(); } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('No forward history item')), - ); - return; + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('No forward history item')), + ); + } } }, ), diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml index 7ccb302a843b..718eb282018b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -4,6 +4,7 @@ publish_to: none environment: sdk: ">=2.12.0 <3.0.0" + flutter: ">=3.0.0" dependencies: flutter: diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h index 2a80c7d886f2..a1c035e40185 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h @@ -3,6 +3,11 @@ // found in the LICENSE file. #import +#import + +NS_ASSUME_NONNULL_BEGIN @interface FLTWebViewFlutterPlugin : NSObject @end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.h new file mode 100644 index 000000000000..297f8c37ec3e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.h @@ -0,0 +1,37 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * App and package facing native API provided by the `webview_flutter_wkwebview` plugin. + * + * This class follows the convention of breaking changes of the Dart API, which means that any + * changes to the class that are not backwards compatible will only be made with a major version + * change of the plugin. Native code other than this external API does not follow breaking change + * conventions, so app or plugin clients should not use any other native APIs. + */ +@interface FWFWebViewFlutterWKWebViewExternalAPI : NSObject +/** + * Retrieves the `WKWebView` that is associated with `identifier`. + * + * See the Dart method `WebKitWebViewController.webViewIdentifier` to get the identifier of an + * underlying `WKWebView`. + * + * @param identifier The associated identifier of the `WebView`. + * @param registry The plugin registry the `FLTWebViewFlutterPlugin` should belong to. If + * the registry doesn't contain an attached instance of `FLTWebViewFlutterPlugin`, + * this method returns nil. + * @return The `WKWebView` associated with `identifier` or nil if a `WKWebView` instance associated + * with `identifier` could not be found. + */ ++ (nullable WKWebView *)webViewForIdentifier:(long)identifier + withPluginRegistry:(id)registry; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.m new file mode 100644 index 000000000000..4e5d6efeb129 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FWFWebViewFlutterWKWebViewExternalAPI.m @@ -0,0 +1,21 @@ +// 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 "FWFWebViewFlutterWKWebViewExternalAPI.h" +#import "FWFInstanceManager.h" + +@implementation FWFWebViewFlutterWKWebViewExternalAPI ++ (nullable WKWebView *)webViewForIdentifier:(long)identifier + withPluginRegistry:(id)registry { + FWFInstanceManager *instanceManager = + (FWFInstanceManager *)[registry valuePublishedByPlugin:@"FLTWebViewFlutterPlugin"]; + + id instance = [instanceManager instanceForIdentifier:identifier]; + if ([instance isKindOfClass:[WKWebView class]]) { + return instance; + } + + return nil; +} +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h index dbcd876d15c9..b9ba942b4ed5 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/webview-umbrella.h @@ -17,5 +17,6 @@ #import #import #import +#import #import #import diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.pigeon.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart similarity index 100% rename from packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.pigeon.dart rename to packages/webview_flutter/webview_flutter_wkwebview/lib/src/common/web_kit.g.dart diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart index d2310e0a5df8..445e232bb0ac 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/foundation/foundation_api_impls.dart @@ -6,7 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import '../common/instance_manager.dart'; -import '../common/web_kit.pigeon.dart'; +import '../common/web_kit.g.dart'; import 'foundation.dart'; Iterable diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart index ae12a11820d8..4749c6afca3c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/ui_kit/ui_kit_api_impls.dart @@ -11,7 +11,7 @@ import 'package:flutter/painting.dart' show Color; import 'package:flutter/services.dart'; import '../common/instance_manager.dart'; -import '../common/web_kit.pigeon.dart'; +import '../common/web_kit.g.dart'; import '../foundation/foundation.dart'; import '../web_kit/web_kit.dart'; import 'ui_kit.dart'; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart index 97a3e0008f81..7cd29da3e716 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/web_kit/web_kit_api_impls.dart @@ -6,11 +6,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import '../common/instance_manager.dart'; -import '../common/web_kit.pigeon.dart'; +import '../common/web_kit.g.dart'; import '../foundation/foundation.dart'; import 'web_kit.dart'; -export '../common/web_kit.pigeon.dart' show WKNavigationType; +export '../common/web_kit.g.dart' show WKNavigationType; Iterable _toWKWebsiteDataTypeEnumData( Iterable types) { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart index 2cdc7e269454..3e8d6796069b 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_proxy.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 'common/instance_manager.dart'; import 'foundation/foundation.dart'; import 'web_kit/web_kit.dart'; @@ -39,10 +40,13 @@ class WebKitProxy { Map change, )? observeValue, + InstanceManager? instanceManager, }) createWebView; /// Constructs a [WKWebViewConfiguration]. - final WKWebViewConfiguration Function() createWebViewConfiguration; + final WKWebViewConfiguration Function({ + InstanceManager? instanceManager, + }) createWebViewConfiguration; /// Constructs a [WKScriptMessageHandler]. final WKScriptMessageHandler Function({ @@ -72,7 +76,7 @@ class WebKitProxy { void Function(WKWebView webView)? webViewWebContentProcessDidTerminate, }) createNavigationDelegate; - /// Contructs a [WKUIDelegate]. + /// Constructs a [WKUIDelegate]. final WKUIDelegate Function({ void Function( WKWebView webView, diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart index dc90906d78f4..8abd0c1afe8a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webkit_webview_controller.dart @@ -49,7 +49,12 @@ class WebKitWebViewControllerCreationParams PlaybackMediaTypes.video, }, this.allowsInlineMediaPlayback = false, - }) : _configuration = webKitProxy.createWebViewConfiguration() { + @visibleForTesting InstanceManager? instanceManager, + }) : _instanceManager = instanceManager ?? NSObject.globalInstanceManager { + _configuration = webKitProxy.createWebViewConfiguration( + instanceManager: _instanceManager, + ); + if (mediaTypesRequiringUserAction.isEmpty) { _configuration.setMediaTypesRequiringUserActionForPlayback( {WKAudiovisualMediaType.none}, @@ -79,13 +84,15 @@ class WebKitWebViewControllerCreationParams PlaybackMediaTypes.video, }, bool allowsInlineMediaPlayback = false, + @visibleForTesting InstanceManager? instanceManager, }) : this( webKitProxy: webKitProxy, mediaTypesRequiringUserAction: mediaTypesRequiringUserAction, allowsInlineMediaPlayback: allowsInlineMediaPlayback, + instanceManager: instanceManager, ); - final WKWebViewConfiguration _configuration; + late final WKWebViewConfiguration _configuration; /// Media types that require a user gesture to begin playing. /// @@ -102,6 +109,10 @@ class WebKitWebViewControllerCreationParams /// native library. @visibleForTesting final WebKitProxy webKitProxy; + + // Maintains instances used to communicate with the native objects they + // represent. + final InstanceManager _instanceManager; } /// An implementation of [PlatformWebViewController] with the WebKit api. @@ -122,12 +133,12 @@ class WebKitWebViewController extends PlatformWebViewController { } /// The WebKit WebView being controlled. - late final WKWebView _webView = withWeakRefenceTo(this, ( - WeakReference weakReference, - ) { - return _webKitParams.webKitProxy.createWebView( - _webKitParams._configuration, - observeValue: ( + late final WKWebView _webView = _webKitParams.webKitProxy.createWebView( + _webKitParams._configuration, + observeValue: withWeakRefenceTo(this, ( + WeakReference weakReference, + ) { + return ( String keyPath, NSObject object, Map change, @@ -139,9 +150,10 @@ class WebKitWebViewController extends PlatformWebViewController { change[NSKeyValueChangeKey.newValue]! as double; progressCallback((progress * 100).round()); } - }, - ); - }); + }; + }), + instanceManager: _webKitParams._instanceManager, + ); final Map _javaScriptChannelParams = {}; @@ -152,6 +164,16 @@ class WebKitWebViewController extends PlatformWebViewController { WebKitWebViewControllerCreationParams get _webKitParams => params as WebKitWebViewControllerCreationParams; + /// Identifier used to retrieve the underlying native `WKWebView`. + /// + /// This is typically used by other plugins to retrieve the native `WKWebView` + /// from an `FWFInstanceManager`. + /// + /// See Objective-C method + /// `FLTWebViewFlutterPlugin:webViewForIdentifier:withPluginRegistry`. + int get webViewIdentifier => + _webKitParams._instanceManager.getIdentifier(_webView)!; + @override Future loadFile(String absoluteFilePath) { return _webView.loadFileUrl( diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart index d32693ee5698..9b334c2411ff 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/pigeons/web_kit.dart @@ -6,8 +6,8 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon( PigeonOptions( - dartOut: 'lib/src/common/web_kit.pigeon.dart', - dartTestOut: 'test/src/common/test_web_kit.pigeon.dart', + dartOut: 'lib/src/common/web_kit.g.dart', + dartTestOut: 'test/src/common/test_web_kit.g.dart', dartOptions: DartOptions(copyrightHeader: [ 'Copyright 2013 The Flutter Authors. All rights reserved.', 'Use of this source code is governed by a BSD-style license that can be', diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index 85440f6e3dfc..d1aaa7cf9203 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_wkwebview description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. repository: https://github.com/flutter/plugins/tree/main/packages/webview_flutter/webview_flutter_wkwebview issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 3.0.2 +version: 3.1.0 environment: sdk: ">=2.17.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.pigeon.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart similarity index 99% rename from packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.pigeon.dart rename to packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart index 73c1053f517d..5c31f63c3add 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.pigeon.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/common/test_web_kit.g.dart @@ -11,7 +11,7 @@ import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart'; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart'; class _TestWKWebsiteDataStoreHostApiCodec extends StandardMessageCodec { const _TestWKWebsiteDataStoreHostApiCodec(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart index 87b659885b52..b9536208c716 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.dart @@ -8,11 +8,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; -import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart'; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation_api_impls.dart'; -import '../common/test_web_kit.pigeon.dart'; +import '../common/test_web_kit.g.dart'; import 'foundation_test.mocks.dart'; @GenerateMocks([ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart index fe80a54ed9ac..d93198ed9d2f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/foundation/foundation_test.mocks.dart @@ -4,10 +4,9 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart' - as _i3; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart' as _i3; -import '../common/test_web_kit.pigeon.dart' as _i2; +import '../common/test_web_kit.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart index f2250e1ac423..f6295668363f 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.dart @@ -12,7 +12,7 @@ import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; -import '../common/test_web_kit.pigeon.dart'; +import '../common/test_web_kit.g.dart'; import 'ui_kit_test.mocks.dart'; @GenerateMocks([ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart index 660c4485ab1b..6200b8dbcadf 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/ui_kit/ui_kit_test.mocks.dart @@ -6,10 +6,9 @@ import 'dart:async' as _i4; import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart' - as _i3; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart' as _i3; -import '../common/test_web_kit.pigeon.dart' as _i2; +import '../common/test_web_kit.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart index 401aebfb8b25..dd007869f0e3 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.dart @@ -9,12 +9,12 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; -import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart'; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit_api_impls.dart'; -import '../common/test_web_kit.pigeon.dart'; +import '../common/test_web_kit.g.dart'; import 'web_kit_test.mocks.dart'; @GenerateMocks([ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart index a1a5bf224596..50e09560ed19 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/src/web_kit/web_kit_test.mocks.dart @@ -6,10 +6,9 @@ import 'dart:async' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:webview_flutter_wkwebview/src/common/web_kit.pigeon.dart' - as _i4; +import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart' as _i4; -import '../common/test_web_kit.pigeon.dart' as _i2; +import '../common/test_web_kit.g.dart' as _i2; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart index fc06db24f055..b7b729a97926 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_controller_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/src/common/instance_manager.dart'; import 'package:webview_flutter_wkwebview/src/foundation/foundation.dart'; import 'package:webview_flutter_wkwebview/src/ui_kit/ui_kit.dart'; import 'package:webview_flutter_wkwebview/src/web_kit/web_kit.dart'; @@ -49,6 +50,7 @@ void main() { })? createMockWebView, MockWKWebViewConfiguration? mockWebViewConfiguration, + InstanceManager? instanceManager, }) { final MockWKWebViewConfiguration nonNullMockWebViewConfiguration = mockWebViewConfiguration ?? MockWKWebViewConfiguration(); @@ -57,7 +59,9 @@ void main() { final PlatformWebViewControllerCreationParams controllerCreationParams = WebKitWebViewControllerCreationParams( webKitProxy: WebKitProxy( - createWebViewConfiguration: () => nonNullMockWebViewConfiguration, + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return nonNullMockWebViewConfiguration; + }, createWebView: ( _, { void Function( @@ -66,6 +70,7 @@ void main() { Map change, )? observeValue, + InstanceManager? instanceManager, }) { nonNullMockWebView = createMockWebView == null ? MockWKWebView() @@ -76,6 +81,7 @@ void main() { return nonNullMockWebView; }, ), + instanceManager: instanceManager, ); final WebKitWebViewController controller = WebKitWebViewController( @@ -104,7 +110,9 @@ void main() { WebKitWebViewControllerCreationParams( webKitProxy: WebKitProxy( - createWebViewConfiguration: () => mockConfiguration, + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, ), allowsInlineMediaPlayback: true, ); @@ -120,7 +128,9 @@ void main() { WebKitWebViewControllerCreationParams( webKitProxy: WebKitProxy( - createWebViewConfiguration: () => mockConfiguration, + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, ), mediaTypesRequiringUserAction: const { PlaybackMediaTypes.video, @@ -143,7 +153,9 @@ void main() { WebKitWebViewControllerCreationParams( webKitProxy: WebKitProxy( - createWebViewConfiguration: () => mockConfiguration, + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, ), ); @@ -164,7 +176,9 @@ void main() { WebKitWebViewControllerCreationParams( webKitProxy: WebKitProxy( - createWebViewConfiguration: () => mockConfiguration, + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return mockConfiguration; + }, ), mediaTypesRequiringUserAction: const {}, ); @@ -922,6 +936,25 @@ void main() { expect(callbackProgress, 0); }); + + test('webViewIdentifier', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final MockWKWebView mockWebView = MockWKWebView(); + when(mockWebView.copy()).thenReturn(MockWKWebView()); + instanceManager.addHostCreatedInstance(mockWebView, 0); + + final WebKitWebViewController controller = createControllerWithMocks( + createMockWebView: (_, {dynamic observeValue}) => mockWebView, + instanceManager: instanceManager, + ); + + expect( + controller.webViewIdentifier, + instanceManager.getIdentifier(mockWebView), + ); + }); }); group('WebKitJavaScriptChannelParams', () { diff --git a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart index 2e0d6e3e9af3..2a6434be4f03 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/test/webkit_webview_widget_test.dart @@ -19,7 +19,7 @@ void main() { group('WebKitWebViewWidget', () { testWidgets('build', (WidgetTester tester) async { - final InstanceManager instanceManager = InstanceManager( + final InstanceManager testInstanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, ); @@ -34,14 +34,17 @@ void main() { Map change, )? observeValue, + InstanceManager? instanceManager, }) { final WKWebView webView = WKWebView.detached( - instanceManager: instanceManager, + instanceManager: testInstanceManager, ); - instanceManager.addDartCreatedInstance(webView); + testInstanceManager.addDartCreatedInstance(webView); return webView; }, - createWebViewConfiguration: () => MockWKWebViewConfiguration(), + createWebViewConfiguration: ({InstanceManager? instanceManager}) { + return MockWKWebViewConfiguration(); + }, ), ), ); @@ -50,7 +53,7 @@ void main() { WebKitWebViewWidgetCreationParams( key: const Key('keyValue'), controller: controller, - instanceManager: instanceManager, + instanceManager: testInstanceManager, ), ); diff --git a/script/configs/exclude_all_plugins_app.yaml b/script/configs/exclude_all_packages_app.yaml similarity index 64% rename from script/configs/exclude_all_plugins_app.yaml rename to script/configs/exclude_all_packages_app.yaml index c116402919b4..8dd0fde5ef5f 100644 --- a/script/configs/exclude_all_plugins_app.yaml +++ b/script/configs/exclude_all_packages_app.yaml @@ -8,8 +8,3 @@ # This is a permament entry, as it should never be a direct app dependency. - plugin_platform_interface -# Temporarily excluded to avoid runtime conflicts with -# path_provider_macos and _ios, which are still what path_provider -# uses. This will be removed when the switch to path_provider_foundation -# is complete. See https://github.com/flutter/flutter/issues/117941 -- path_provider_foundation diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 55b5aeb7222a..3c4905ad7071 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.13.4+1 + +* Makes `--packages-for-branch` detect any commit on `main` as being `main`, + so that it works with pinned checkouts (e.g., on LUCI). + +## 0.13.4 + +* Adds the ability to validate minimum supported Dart/Flutter versions in + `pubspec-check`. + ## 0.13.3 * Renames `podspecs` to `podspec-check`. The old name will continue to work. diff --git a/script/tool/README.md b/script/tool/README.md index 9f0ac84145f2..aa4c0517ce71 100644 --- a/script/tool/README.md +++ b/script/tool/README.md @@ -1,191 +1,13 @@ -# Flutter Plugin Tools +# Removed -This is a set of utilities used in the flutter/plugins and flutter/packages -repositories. It is no longer explictily maintained as a general-purpose tool -for multi-package repositories, so your mileage may vary if using it in other -repositories. +See https://github.com/flutter/packages/blob/main/script/tool/README.md for the +current location of this tooling. -Note: The commands in tools are designed to run at the root of the repository or `/packages/`. +## Temporary shim -## Getting Started +This is a temporary, minimal version of the tools sufficient to keep the +following scripts running until the repository merge is complete and they are +updated to use flutter/packages instead: -In flutter/plugins, the tool is run from source. In flutter/packages, the -[published version](https://pub.dev/packages/flutter_plugin_tools) is used -instead. (It is marked as Discontinued since it is no longer maintained as -a general-purpose tool, but updates are still published for use in -flutter/packages.) - -The commands in tools require the Flutter-bundled version of Dart to be the first `dart` loaded in the path. - -### Extra Setup - -When updating sample code excerpts (`update-excerpts`) for the README.md files, -there is some [extra setup for -submodules](#update-readmemd-from-example-sources) that is necessary. - -### From Source (flutter/plugins only) - -Set up: - -```sh -cd ./script/tool && dart pub get && cd ../../ -``` - -Run: - -```sh -dart run ./script/tool/bin/flutter_plugin_tools.dart -``` - -### Published Version - -Set up: - -```sh -dart pub global activate flutter_plugin_tools -``` - -Run: - -```sh -dart pub global run flutter_plugin_tools -``` - -## Commands - -Run with `--help` for a full list of commands and arguments, but the -following shows a number of common commands being run for a specific package. - -All examples assume running from source; see above for running the -published version instead. - -Most commands take a `--packages` argument to control which package(s) the -command is targetting. An package name can be any of: -- The name of a package (e.g., `path_provider_android`). -- The name of a federated plugin (e.g., `path_provider`), in which case all - packages that make up that plugin will be targetted. -- A combination federated_plugin_name/package_name (e.g., - `path_provider/path_provider` for the app-facing package). - -### Format Code - -```sh -cd -dart run ./script/tool/bin/flutter_plugin_tools.dart format --packages package_name -``` - -### Run the Dart Static Analyzer - -```sh -cd -dart run ./script/tool/bin/flutter_plugin_tools.dart analyze --packages package_name -``` - -### Run Dart Unit Tests - -```sh -cd -dart run ./script/tool/bin/flutter_plugin_tools.dart test --packages package_name -``` - -### Run Dart Integration Tests - -```sh -cd -dart run ./script/tool/bin/flutter_plugin_tools.dart build-examples --apk --packages package_name -dart run ./script/tool/bin/flutter_plugin_tools.dart drive-examples --android --packages package_name -``` - -Replace `--apk`/`--android` with the platform you want to test against -(omit it to get a list of valid options). - -### Run Native Tests - -`native-test` takes one or more platform flags to run tests for. By default it -runs both unit tests and (on platforms that support it) integration tests, but -`--no-unit` or `--no-integration` can be used to run just one type. - -Examples: - -```sh -cd -# Run just unit tests for iOS and Android: -dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --ios --android --no-integration --packages package_name -# Run all tests for macOS: -dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --macos --packages package_name -# Run all tests for Windows: -dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --windows --packages package_name -``` - -### Update README.md from Example Sources - -`update-excerpts` requires sources that are in a submodule. If you didn't clone -with submodules, you will need to `git submodule update --init --recursive` -before running this command. - -```sh -cd -dart run ./script/tool/bin/flutter_plugin_tools.dart update-excerpts --packages package_name -``` - -### Update CHANGELOG and Version - -`update-release-info` will automatically update the version and `CHANGELOG.md` -following standard repository style and practice. It can be used for -single-package updates to handle the details of getting the `CHANGELOG.md` -format correct, but is especially useful for bulk updates across multiple packages. - -For instance, if you add a new analysis option that requires production -code changes across many packages: - -```sh -cd -dart run ./script/tool/bin/flutter_plugin_tools.dart update-release-info \ - --version=minimal \ - --changelog="Fixes violations of new analysis option some_new_option." -``` - -The `minimal` option for `--version` will skip unchanged packages, and treat -each changed package as either `bugfix` or `next` depending on the files that -have changed in that package, so it is often the best choice for a bulk change. - -For cases where you know the change time, `minor` or `bugfix` will make the -corresponding version bump, or `next` will update only `CHANGELOG.md` without -changing the version. - -### Publish a Release - -**Releases are automated for `flutter/plugins` and `flutter/packages`.** - -The manual procedure described here is _deprecated_, and should only be used when -the automated process fails. Please, read -[Releasing a Plugin or Package](https://github.com/flutter/flutter/wiki/Releasing-a-Plugin-or-Package) -on the Flutter Wiki first. - -```sh -cd -git checkout -dart run ./script/tool/bin/flutter_plugin_tools.dart publish --packages -``` - -By default the tool tries to push tags to the `upstream` remote, but some -additional settings can be configured. Run `dart run ./script/tool/bin/flutter_plugin_tools.dart -publish --help` for more usage information. - -The tool wraps `pub publish` for pushing the package to pub, and then will -automatically use git to try to create and push tags. It has some additional -safety checking around `pub publish` too. By default `pub publish` publishes -_everything_, including untracked or uncommitted files in version control. -`publish` will first check the status of the local -directory and refuse to publish if there are any mismatched files with version -control present. - -## Updating the Tool - -For flutter/plugins, just changing the source here is all that's needed. - -For changes that are relevant to flutter/packages, you will also need to: -- Update the tool's pubspec.yaml and CHANGELOG -- Publish the tool -- Update the pinned version in - [flutter/packages](https://github.com/flutter/packages/blob/main/.cirrus.yml) +- [dart-lang analysis](https://github.com/dart-lang/sdk/blob/main/tools/bots/flutter/analyze_flutter_plugins.sh) +- [flutter/flutter analysis](https://github.com/flutter/flutter/blob/master/dev/bots/test.dart) diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index c7a953c50cac..3d9e4e5c9802 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -32,17 +32,9 @@ class AnalyzeCommand extends PackageLoopingCommand { valueHelp: 'dart-sdk', help: 'An optional path to a Dart SDK; this is used to override the ' 'SDK used to provide analysis.'); - argParser.addFlag(_downgradeFlag, - help: 'Runs "flutter pub downgrade" before analysis to verify that ' - 'the minimum constraints are sufficiently new for APIs used.'); - argParser.addFlag(_libOnlyFlag, - help: 'Only analyze the lib/ directory of the main package, not the ' - 'entire package.'); } static const String _customAnalysisFlag = 'custom-analysis'; - static const String _downgradeFlag = 'downgrade'; - static const String _libOnlyFlag = 'lib-only'; static const String _analysisSdk = 'analysis-sdk'; late String _dartBinaryPath; @@ -111,18 +103,6 @@ class AnalyzeCommand extends PackageLoopingCommand { @override Future runForPackage(RepositoryPackage package) async { - final bool libOnly = getBoolArg(_libOnlyFlag); - - if (libOnly && !package.libDirectory.existsSync()) { - return PackageResult.skip('No lib/ directory.'); - } - - if (getBoolArg(_downgradeFlag)) { - if (!await _runPubCommand(package, 'downgrade')) { - return PackageResult.fail(['Unable to downgrade dependencies']); - } - } - // Analysis runs over the package and all subpackages (unless only lib/ is // being analyzed), so all of them need `flutter pub get` run before // analyzing. `example` packages can be skipped since 'flutter packages get' @@ -130,7 +110,7 @@ class AnalyzeCommand extends PackageLoopingCommand { // directory. final List packagesToGet = [ package, - if (!libOnly) ...await getSubpackages(package).toList(), + ...await getSubpackages(package).toList(), ]; for (final RepositoryPackage packageToGet in packagesToGet) { if (packageToGet.directory.basename != 'example' || @@ -146,8 +126,8 @@ class AnalyzeCommand extends PackageLoopingCommand { if (_hasUnexpecetdAnalysisOptions(package)) { return PackageResult.fail(['Unexpected local analysis options']); } - final int exitCode = await processRunner.runAndStream(_dartBinaryPath, - ['analyze', '--fatal-infos', if (libOnly) 'lib'], + final int exitCode = await processRunner.runAndStream( + _dartBinaryPath, ['analyze', '--fatal-infos'], workingDir: package.directory); if (exitCode != 0) { return PackageResult.fail(); diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart deleted file mode 100644 index 1aade3575559..000000000000 --- a/script/tool/lib/src/build_examples_command.dart +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; -import 'package:yaml/yaml.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// Key for APK. -const String _platformFlagApk = 'apk'; - -const String _pluginToolsConfigFileName = '.pluginToolsConfig.yaml'; -const String _pluginToolsConfigBuildFlagsKey = 'buildFlags'; -const String _pluginToolsConfigGlobalKey = 'global'; - -const String _pluginToolsConfigExample = ''' -$_pluginToolsConfigBuildFlagsKey: - $_pluginToolsConfigGlobalKey: - - "--no-tree-shake-icons" - - "--dart-define=buildmode=testing" -'''; - -const int _exitNoPlatformFlags = 3; -const int _exitInvalidPluginToolsConfig = 4; - -// Flutter build types. These are the values passed to `flutter build `. -const String _flutterBuildTypeAndroid = 'apk'; -const String _flutterBuildTypeIOS = 'ios'; -const String _flutterBuildTypeLinux = 'linux'; -const String _flutterBuildTypeMacOS = 'macos'; -const String _flutterBuildTypeWeb = 'web'; -const String _flutterBuildTypeWindows = 'windows'; - -/// A command to build the example applications for packages. -class BuildExamplesCommand extends PackageLoopingCommand { - /// Creates an instance of the build command. - BuildExamplesCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag(platformLinux); - argParser.addFlag(platformMacOS); - argParser.addFlag(platformWeb); - argParser.addFlag(platformWindows); - argParser.addFlag(platformIOS); - argParser.addFlag(_platformFlagApk); - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: 'Enables the given Dart SDK experiments.', - ); - } - - // Maps the switch this command uses to identify a platform to information - // about it. - static final Map _platforms = - { - _platformFlagApk: const _PlatformDetails( - 'Android', - pluginPlatform: platformAndroid, - flutterBuildType: _flutterBuildTypeAndroid, - ), - platformIOS: const _PlatformDetails( - 'iOS', - pluginPlatform: platformIOS, - flutterBuildType: _flutterBuildTypeIOS, - extraBuildFlags: ['--no-codesign'], - ), - platformLinux: const _PlatformDetails( - 'Linux', - pluginPlatform: platformLinux, - flutterBuildType: _flutterBuildTypeLinux, - ), - platformMacOS: const _PlatformDetails( - 'macOS', - pluginPlatform: platformMacOS, - flutterBuildType: _flutterBuildTypeMacOS, - ), - platformWeb: const _PlatformDetails( - 'web', - pluginPlatform: platformWeb, - flutterBuildType: _flutterBuildTypeWeb, - ), - platformWindows: const _PlatformDetails( - 'Windows', - pluginPlatform: platformWindows, - flutterBuildType: _flutterBuildTypeWindows, - ), - }; - - @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.\n\n' - 'A $_pluginToolsConfigFileName file can be placed in an example app ' - 'directory to specify additional build arguments. It should be a YAML ' - 'file with a top-level map containing a single key ' - '"$_pluginToolsConfigBuildFlagsKey" containing a map containing a ' - 'single key "$_pluginToolsConfigGlobalKey" containing a list of build ' - 'arguments.'; - - @override - Future initializeRun() async { - final List platformFlags = _platforms.keys.toList(); - platformFlags.sort(); - if (!platformFlags.any((String platform) => getBoolArg(platform))) { - printError( - 'None of ${platformFlags.map((String platform) => '--$platform').join(', ')} ' - 'were specified. At least one platform must be provided.'); - throw ToolExit(_exitNoPlatformFlags); - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - final List errors = []; - - final bool isPlugin = isFlutterPlugin(package); - final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries - .where( - (MapEntry entry) => getBoolArg(entry.key)) - .map((MapEntry entry) => entry.value); - - // Platform support is checked at the package level for plugins; there is - // no package-level platform information for non-plugin packages. - final Set<_PlatformDetails> buildPlatforms = isPlugin - ? requestedPlatforms - .where((_PlatformDetails platform) => - pluginSupportsPlatform(platform.pluginPlatform, package)) - .toSet() - : requestedPlatforms.toSet(); - - String platformDisplayList(Iterable<_PlatformDetails> platforms) { - return platforms.map((_PlatformDetails p) => p.label).join(', '); - } - - if (buildPlatforms.isEmpty) { - final String unsupported = requestedPlatforms.length == 1 - ? '${requestedPlatforms.first.label} is not supported' - : 'None of [${platformDisplayList(requestedPlatforms)}] are supported'; - return PackageResult.skip('$unsupported by this plugin'); - } - print('Building for: ${platformDisplayList(buildPlatforms)}'); - - final Set<_PlatformDetails> unsupportedPlatforms = - requestedPlatforms.toSet().difference(buildPlatforms); - if (unsupportedPlatforms.isNotEmpty) { - final List skippedPlatforms = unsupportedPlatforms - .map((_PlatformDetails platform) => platform.label) - .toList(); - skippedPlatforms.sort(); - print('Skipping unsupported platform(s): ' - '${skippedPlatforms.join(', ')}'); - } - print(''); - - bool builtSomething = false; - for (final RepositoryPackage example in package.getExamples()) { - final String packageName = - getRelativePosixPath(example.directory, from: packagesDir); - - for (final _PlatformDetails platform in buildPlatforms) { - // Repo policy is that a plugin must have examples configured for all - // supported platforms. For packages, just log and skip any requested - // platform that a package doesn't have set up. - if (!isPlugin && - !example.directory - .childDirectory(platform.flutterPlatformDirectory) - .existsSync()) { - print('Skipping ${platform.label} for $packageName; not supported.'); - continue; - } - - builtSomething = true; - - String buildPlatform = platform.label; - if (platform.label.toLowerCase() != platform.flutterBuildType) { - buildPlatform += ' (${platform.flutterBuildType})'; - } - print('\nBUILDING $packageName for $buildPlatform'); - if (!await _buildExample(example, platform.flutterBuildType, - extraBuildFlags: platform.extraBuildFlags)) { - errors.add('$packageName (${platform.label})'); - } - } - } - - if (!builtSomething) { - if (isPlugin) { - errors.add('No examples found'); - } else { - return PackageResult.skip( - 'No examples found supporting requested platform(s).'); - } - } - - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - Iterable _readExtraBuildFlagsConfiguration( - Directory directory) sync* { - final File pluginToolsConfig = - directory.childFile(_pluginToolsConfigFileName); - if (pluginToolsConfig.existsSync()) { - final Object? configuration = - loadYaml(pluginToolsConfig.readAsStringSync()); - if (configuration is! YamlMap) { - printError('The $_pluginToolsConfigFileName file must be a YAML map.'); - printError( - 'Currently, the key "$_pluginToolsConfigBuildFlagsKey" is the only one that has an effect.'); - printError( - 'It must itself be a map. Currently, in that map only the key "$_pluginToolsConfigGlobalKey"'); - printError( - 'has any effect; it must contain a list of arguments to pass to the'); - printError('flutter tool.'); - printError(_pluginToolsConfigExample); - throw ToolExit(_exitInvalidPluginToolsConfig); - } - if (configuration.containsKey(_pluginToolsConfigBuildFlagsKey)) { - final Object? buildFlagsConfiguration = - configuration[_pluginToolsConfigBuildFlagsKey]; - if (buildFlagsConfiguration is! YamlMap) { - printError( - 'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map.'); - printError( - 'Currently, in that map only the key "$_pluginToolsConfigGlobalKey" has any effect; it must '); - printError( - 'contain a list of arguments to pass to the flutter tool.'); - printError(_pluginToolsConfigExample); - throw ToolExit(_exitInvalidPluginToolsConfig); - } - if (buildFlagsConfiguration.containsKey(_pluginToolsConfigGlobalKey)) { - final Object? globalBuildFlagsConfiguration = - buildFlagsConfiguration[_pluginToolsConfigGlobalKey]; - if (globalBuildFlagsConfiguration is! YamlList) { - printError( - 'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map'); - printError('whose "$_pluginToolsConfigGlobalKey" key is a list.'); - printError( - 'That list must contain a list of arguments to pass to the flutter tool.'); - printError( - 'For example, the $_pluginToolsConfigFileName file could look like:'); - printError(_pluginToolsConfigExample); - throw ToolExit(_exitInvalidPluginToolsConfig); - } - yield* globalBuildFlagsConfiguration.cast(); - } - } - } - } - - Future _buildExample( - RepositoryPackage example, - String flutterBuildType, { - List extraBuildFlags = const [], - }) async { - final String enableExperiment = getStringArg(kEnableExperiment); - - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - flutterBuildType, - ...extraBuildFlags, - ..._readExtraBuildFlagsConfiguration(example.directory), - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - ], - workingDir: example.directory, - ); - return exitCode == 0; - } -} - -/// A collection of information related to a specific platform. -class _PlatformDetails { - const _PlatformDetails( - this.label, { - required this.pluginPlatform, - required this.flutterBuildType, - this.extraBuildFlags = const [], - }); - - /// The name to use in output. - final String label; - - /// The key in a pubspec's platform: entry. - final String pluginPlatform; - - /// The `flutter build` build type. - final String flutterBuildType; - - /// The Flutter platform directory name. - // In practice, this is the same as the plugin platform key for all platforms. - // If that changes, this can be adjusted. - String get flutterPlatformDirectory => pluginPlatform; - - /// Any extra flags to pass to `flutter build`. - final List extraBuildFlags; -} diff --git a/script/tool/lib/src/common/cmake.dart b/script/tool/lib/src/common/cmake.dart deleted file mode 100644 index 3f5d8452bd44..000000000000 --- a/script/tool/lib/src/common/cmake.dart +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'core.dart'; -import 'process_runner.dart'; - -const String _cacheCommandKey = 'CMAKE_COMMAND:INTERNAL'; - -/// A utility class for interacting with CMake projects. -class CMakeProject { - /// Creates an instance that runs commands for [project] with the given - /// [processRunner]. - CMakeProject( - this.flutterProject, { - required this.buildMode, - this.processRunner = const ProcessRunner(), - this.platform = const LocalPlatform(), - }); - - /// The directory of a Flutter project to run Gradle commands in. - final Directory flutterProject; - - /// The [ProcessRunner] used to run commands. Overridable for testing. - final ProcessRunner processRunner; - - /// The platform that commands are being run on. - final Platform platform; - - /// The build mode (e.g., Debug, Release). - /// - /// This is a constructor paramater because on Linux many properties depend - /// on the build mode since it uses a single-configuration generator. - final String buildMode; - - late final String _cmakeCommand = _determineCmakeCommand(); - - /// The project's platform directory name. - String get _platformDirName => platform.isWindows ? 'windows' : 'linux'; - - /// The project's 'example' build directory for this instance's platform. - Directory get buildDirectory { - Directory buildDir = - flutterProject.childDirectory('build').childDirectory(_platformDirName); - if (platform.isLinux) { - buildDir = buildDir - // TODO(stuartmorgan): Support arm64 if that ever becomes a supported - // CI configuration for the repository. - .childDirectory('x64') - // Linux uses a single-config generator, so the base build directory - // includes the configuration. - .childDirectory(buildMode.toLowerCase()); - } - return buildDir; - } - - File get _cacheFile => buildDirectory.childFile('CMakeCache.txt'); - - /// Returns the CMake command to run build commands for this project. - /// - /// Assumes the project has been built at least once, such that the CMake - /// generation step has run. - String getCmakeCommand() { - return _cmakeCommand; - } - - /// Returns the CMake command to run build commands for this project. This is - /// used to initialize _cmakeCommand, and should not be called directly. - /// - /// Assumes the project has been built at least once, such that the CMake - /// generation step has run. - String _determineCmakeCommand() { - // On Linux 'cmake' is expected to be in the path, so doesn't need to - // be lookup up and cached. - if (platform.isLinux) { - return 'cmake'; - } - final File cacheFile = _cacheFile; - String? command; - for (String line in cacheFile.readAsLinesSync()) { - line = line.trim(); - if (line.startsWith(_cacheCommandKey)) { - command = line.substring(line.indexOf('=') + 1).trim(); - break; - } - } - if (command == null) { - printError('Unable to find CMake command in ${cacheFile.path}'); - throw ToolExit(100); - } - return command; - } - - /// Whether or not the project is ready to have CMake commands run on it - /// (i.e., whether the `flutter` tool has generated the necessary files). - bool isConfigured() => _cacheFile.existsSync(); - - /// Runs a `cmake` command with the given parameters. - Future runBuild( - String target, { - List arguments = const [], - }) { - return processRunner.runAndStream( - getCmakeCommand(), - [ - '--build', - buildDirectory.path, - '--target', - target, - if (platform.isWindows) ...['--config', buildMode], - ...arguments, - ], - ); - } -} diff --git a/script/tool/lib/src/common/file_utils.dart b/script/tool/lib/src/common/file_utils.dart deleted file mode 100644 index 3c2f2f18f954..000000000000 --- a/script/tool/lib/src/common/file_utils.dart +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; - -/// Returns a [File] created by appending all but the last item in [components] -/// to [base] as subdirectories, then appending the last as a file. -/// -/// Example: -/// childFileWithSubcomponents(rootDir, ['foo', 'bar', 'baz.txt']) -/// creates a File representing /rootDir/foo/bar/baz.txt. -File childFileWithSubcomponents(Directory base, List components) { - Directory dir = base; - final String basename = components.removeLast(); - for (final String directoryName in components) { - dir = dir.childDirectory(directoryName); - } - return dir.childFile(basename); -} diff --git a/script/tool/lib/src/common/gradle.dart b/script/tool/lib/src/common/gradle.dart deleted file mode 100644 index 746536075014..000000000000 --- a/script/tool/lib/src/common/gradle.dart +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'process_runner.dart'; -import 'repository_package.dart'; - -const String _gradleWrapperWindows = 'gradlew.bat'; -const String _gradleWrapperNonWindows = 'gradlew'; - -/// A utility class for interacting with Gradle projects. -class GradleProject { - /// Creates an instance that runs commands for [project] with the given - /// [processRunner]. - GradleProject( - this.flutterProject, { - this.processRunner = const ProcessRunner(), - this.platform = const LocalPlatform(), - }); - - /// The directory of a Flutter project to run Gradle commands in. - final RepositoryPackage flutterProject; - - /// The [ProcessRunner] used to run commands. Overridable for testing. - final ProcessRunner processRunner; - - /// The platform that commands are being run on. - final Platform platform; - - /// The project's 'android' directory. - Directory get androidDirectory => - flutterProject.platformDirectory(FlutterPlatform.android); - - /// The path to the Gradle wrapper file for the project. - File get gradleWrapper => androidDirectory.childFile( - platform.isWindows ? _gradleWrapperWindows : _gradleWrapperNonWindows); - - /// Whether or not the project is ready to have Gradle commands run on it - /// (i.e., whether the `flutter` tool has generated the necessary files). - bool isConfigured() => gradleWrapper.existsSync(); - - /// Runs a `gradlew` command with the given parameters. - Future runCommand( - String target, { - List arguments = const [], - }) { - return processRunner.runAndStream( - gradleWrapper.path, - [target, ...arguments], - workingDir: androidDirectory, - ); - } -} diff --git a/script/tool/lib/src/common/package_command.dart b/script/tool/lib/src/common/package_command.dart index 0e83d03e9846..8a2bbfc40058 100644 --- a/script/tool/lib/src/common/package_command.dart +++ b/script/tool/lib/src/common/package_command.dart @@ -316,17 +316,28 @@ abstract class PackageCommand extends Command { } else if (getBoolArg(_packagesForBranchArg)) { final String? branch = await _getBranch(); if (branch == null) { - printError('Unabled to determine branch; --$_packagesForBranchArg can ' + printError('Unable to determine branch; --$_packagesForBranchArg can ' 'only be used in a git repository.'); throw ToolExit(exitInvalidArguments); } else { // Configure the change finder the correct mode for the branch. - final bool lastCommitOnly = branch == 'main' || branch == 'master'; + // Log the mode to make it easier to audit logs to see that the + // intended diff was used (or why). + final bool lastCommitOnly; + if (branch == 'main' || branch == 'master') { + print('--$_packagesForBranchArg: running on default branch.'); + lastCommitOnly = true; + } else if (await _isCheckoutFromBranch('main')) { + print( + '--$_packagesForBranchArg: running on a commit from default branch.'); + lastCommitOnly = true; + } else { + print('--$_packagesForBranchArg: running on branch "$branch".'); + lastCommitOnly = false; + } if (lastCommitOnly) { - // Log the mode to make it easier to audit logs to see that the - // intended diff was used. - print('--$_packagesForBranchArg: running on default branch; ' - 'using parent commit as the diff base.'); + print( + '--$_packagesForBranchArg: using parent commit as the diff base.'); changedFileFinder = GitVersionFinder(await gitDir, 'HEAD~'); } else { changedFileFinder = await retrieveVersionFinder(); @@ -522,6 +533,35 @@ abstract class PackageCommand extends Command { return packages; } + // Returns true if the current checkout is on an ancestor of [branch]. + // + // This is used because CI may check out a specific hash rather than a branch, + // in which case branch-name detection won't work. + Future _isCheckoutFromBranch(String branchName) async { + // The target branch may not exist locally; try some common remote names for + // the branch as well. + final List candidateBranchNames = [ + branchName, + 'origin/$branchName', + 'upstream/$branchName', + ]; + for (final String branch in candidateBranchNames) { + final io.ProcessResult result = await (await gitDir).runCommand( + ['merge-base', '--is-ancestor', 'HEAD', branch], + throwOnError: false); + if (result.exitCode == 0) { + return true; + } else if (result.exitCode == 1) { + // 1 indicates that the branch was successfully checked, but it's not + // an ancestor. + return false; + } + // Any other return code is an error, such as `branch` not being a valid + // name in the repository, so try other name variants. + } + return false; + } + Future _getBranch() async { final io.ProcessResult branchResult = await (await gitDir).runCommand( ['rev-parse', '--abbrev-ref', 'HEAD'], diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart deleted file mode 100644 index 464dac6c18d6..000000000000 --- a/script/tool/lib/src/common/package_state_utils.dart +++ /dev/null @@ -1,225 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; - -import 'git_version_finder.dart'; -import 'repository_package.dart'; - -/// The state of a package on disk relative to git state. -@immutable -class PackageChangeState { - /// Creates a new immutable state instance. - const PackageChangeState({ - required this.hasChanges, - required this.hasChangelogChange, - required this.needsChangelogChange, - required this.needsVersionChange, - }); - - /// True if there are any changes to files in the package. - final bool hasChanges; - - /// True if the package's CHANGELOG.md has been changed. - final bool hasChangelogChange; - - /// True if any changes in the package require a version change according - /// to repository policy. - final bool needsVersionChange; - - /// True if any changes in the package require a CHANGELOG change according - /// to repository policy. - final bool needsChangelogChange; -} - -/// Checks [package] against [changedPaths] to determine what changes it has -/// and how those changes relate to repository policy about CHANGELOG and -/// version updates. -/// -/// [changedPaths] should be a list of POSIX-style paths from a common root, -/// and [relativePackagePath] should be the path to [package] from that same -/// root. Commonly these will come from `gitVersionFinder.getChangedFiles()` -/// and `getRelativePosixPath(package.directory, gitDir.path)` respectively; -/// they are arguments mainly to allow for caching the changed paths for an -/// entire command run. -/// -/// If [git] is provided, [changedPaths] must be repository-relative -/// paths, and change type detection can use file diffs in addition to paths. -Future checkPackageChangeState( - RepositoryPackage package, { - required List changedPaths, - required String relativePackagePath, - GitVersionFinder? git, -}) async { - final String packagePrefix = relativePackagePath.endsWith('/') - ? relativePackagePath - : '$relativePackagePath/'; - - bool hasChanges = false; - bool hasChangelogChange = false; - bool needsVersionChange = false; - bool needsChangelogChange = false; - for (final String path in changedPaths) { - // Only consider files within the package. - if (!path.startsWith(packagePrefix)) { - continue; - } - final String packageRelativePath = path.substring(packagePrefix.length); - hasChanges = true; - - final List components = p.posix.split(packageRelativePath); - if (components.isEmpty) { - continue; - } - - if (components.first == 'CHANGELOG.md') { - hasChangelogChange = true; - continue; - } - - if (!needsVersionChange) { - // Developer-only changes don't need version changes or changelog changes. - if (await _isDevChange(components, git: git, repoPath: path)) { - continue; - } - - // Some other changes don't need version changes, but might benefit from - // changelog changes. - needsChangelogChange = true; - if ( - // One of a few special files example will be shown on pub.dev, but - // for anything else in the example publishing has no purpose. - !_isUnpublishedExampleChange(components, package)) { - needsVersionChange = true; - } - } - } - - return PackageChangeState( - hasChanges: hasChanges, - hasChangelogChange: hasChangelogChange, - needsChangelogChange: needsChangelogChange, - needsVersionChange: needsVersionChange); -} - -bool _isTestChange(List pathComponents) { - return pathComponents.contains('test') || - pathComponents.contains('integration_test') || - pathComponents.contains('androidTest') || - pathComponents.contains('RunnerTests') || - pathComponents.contains('RunnerUITests') || - // Pigeon's custom platform tests. - pathComponents.first == 'platform_tests'; -} - -// True if the given file is an example file other than the one that will be -// published according to https://dart.dev/tools/pub/package-layout#examples. -// -// This is not exhastive; it currently only handles variations we actually have -// in our repositories. -bool _isUnpublishedExampleChange( - List pathComponents, RepositoryPackage package) { - if (pathComponents.first != 'example') { - return false; - } - final List exampleComponents = pathComponents.sublist(1); - if (exampleComponents.isEmpty) { - return false; - } - - final Directory exampleDirectory = - package.directory.childDirectory('example'); - - // Check for example.md/EXAMPLE.md first, as that has priority. If it's - // present, any other example file is unpublished. - final bool hasExampleMd = - exampleDirectory.childFile('example.md').existsSync() || - exampleDirectory.childFile('EXAMPLE.md').existsSync(); - if (hasExampleMd) { - return !(exampleComponents.length == 1 && - exampleComponents.first.toLowerCase() == 'example.md'); - } - - // Most packages have an example/lib/main.dart (or occasionally - // example/main.dart), so check for that. The other naming variations aren't - // currently used. - const String mainName = 'main.dart'; - final bool hasExampleCode = - exampleDirectory.childDirectory('lib').childFile(mainName).existsSync() || - exampleDirectory.childFile(mainName).existsSync(); - if (hasExampleCode) { - // If there is an example main, only that example file is published. - return !((exampleComponents.length == 1 && - exampleComponents.first == mainName) || - (exampleComponents.length == 2 && - exampleComponents.first == 'lib' && - exampleComponents[1] == mainName)); - } - - // If there's no example code either, the example README.md, if any, is the - // file that will be published. - return exampleComponents.first.toLowerCase() != 'readme.md'; -} - -// True if the change is only relevant to people working on the package. -Future _isDevChange(List pathComponents, - {GitVersionFinder? git, String? repoPath}) async { - return _isTestChange(pathComponents) || - // The top-level "tool" directory is for non-client-facing utility - // code, such as test scripts. - pathComponents.first == 'tool' || - // Entry point for the 'custom-test' command, which is only for CI and - // local testing. - pathComponents.first == 'run_tests.sh' || - // Ignoring lints doesn't affect clients. - pathComponents.contains('lint-baseline.xml') || - // Example build files are very unlikely to be interesting to clients. - _isExampleBuildFile(pathComponents) || - // Test-only gradle depenedencies don't affect clients. - await _isGradleTestDependencyChange(pathComponents, - git: git, repoPath: repoPath); -} - -bool _isExampleBuildFile(List pathComponents) { - if (!pathComponents.contains('example')) { - return false; - } - return pathComponents.contains('gradle-wrapper.properties') || - pathComponents.contains('gradle.properties') || - pathComponents.contains('build.gradle') || - pathComponents.contains('Runner.xcodeproj') || - pathComponents.contains('CMakeLists.txt') || - pathComponents.contains('pubspec.yaml'); -} - -Future _isGradleTestDependencyChange(List pathComponents, - {GitVersionFinder? git, String? repoPath}) async { - if (git == null) { - return false; - } - if (pathComponents.last != 'build.gradle') { - return false; - } - final List diff = await git.getDiffContents(targetPath: repoPath); - final RegExp changeLine = RegExp(r'[+-] '); - final RegExp testDependencyLine = - RegExp(r'[+-]\s*(?:androidT|t)estImplementation\s'); - bool foundTestDependencyChange = false; - for (final String line in diff) { - if (!changeLine.hasMatch(line) || - line.startsWith('--- ') || - line.startsWith('+++ ')) { - continue; - } - if (!testDependencyLine.hasMatch(line)) { - return false; - } - foundTestDependencyChange = true; - } - // Only return true if a test dependency change was found, as a failsafe - // against having the wrong (e.g., incorrectly empty) diff output. - return foundTestDependencyChange; -} diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart deleted file mode 100644 index 94677fe7e5a3..000000000000 --- a/script/tool/lib/src/common/plugin_utils.dart +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:yaml/yaml.dart'; - -import 'core.dart'; -import 'repository_package.dart'; - -/// Possible plugin support options for a platform. -enum PlatformSupport { - /// The platform has an implementation in the package. - inline, - - /// The platform has an endorsed federated implementation in another package. - federated, -} - -/// Returns true if [package] is a Flutter plugin. -bool isFlutterPlugin(RepositoryPackage package) { - return _readPluginPubspecSection(package) != null; -} - -/// Returns true if [package] is a Flutter [platform] plugin. -/// -/// It checks this by looking for the following pattern in the pubspec: -/// -/// flutter: -/// plugin: -/// platforms: -/// [platform]: -/// -/// If [requiredMode] is provided, the plugin must have the given type of -/// implementation in order to return true. -bool pluginSupportsPlatform( - String platform, - RepositoryPackage plugin, { - PlatformSupport? requiredMode, -}) { - assert(platform == platformIOS || - platform == platformAndroid || - platform == platformWeb || - platform == platformMacOS || - platform == platformWindows || - platform == platformLinux); - - final YamlMap? platformEntry = - _readPlatformPubspecSectionForPlugin(platform, plugin); - if (platformEntry == null) { - return false; - } - - // If the platform entry is present, then it supports the platform. Check - // for required mode if specified. - if (requiredMode != null) { - final bool federated = platformEntry.containsKey('default_package'); - if (federated != (requiredMode == PlatformSupport.federated)) { - return false; - } - } - - return true; -} - -/// Returns true if [plugin] includes native code for [platform], as opposed to -/// being implemented entirely in Dart. -bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) { - if (platform == platformWeb) { - // Web plugins are always Dart-only. - return false; - } - final YamlMap? platformEntry = - _readPlatformPubspecSectionForPlugin(platform, plugin); - if (platformEntry == null) { - return false; - } - // All other platforms currently use pluginClass for indicating the native - // code in the plugin. - final String? pluginClass = platformEntry['pluginClass'] as String?; - // TODO(stuartmorgan): Remove the check for 'none' once none of the plugins - // in the repository use that workaround. See - // https://github.com/flutter/flutter/issues/57497 for context. - return pluginClass != null && pluginClass != 'none'; -} - -/// Returns the -/// flutter: -/// plugin: -/// platforms: -/// [platform]: -/// section from [plugin]'s pubspec.yaml, or null if either it is not present, -/// or the pubspec couldn't be read. -YamlMap? _readPlatformPubspecSectionForPlugin( - String platform, RepositoryPackage plugin) { - final YamlMap? pluginSection = _readPluginPubspecSection(plugin); - if (pluginSection == null) { - return null; - } - final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; - if (platforms == null) { - return null; - } - return platforms[platform] as YamlMap?; -} - -/// Returns the -/// flutter: -/// plugin: -/// platforms: -/// section from [plugin]'s pubspec.yaml, or null if either it is not present, -/// or the pubspec couldn't be read. -YamlMap? _readPluginPubspecSection(RepositoryPackage package) { - final Pubspec pubspec = package.parsePubspec(); - final Map? flutterSection = pubspec.flutter; - if (flutterSection == null) { - return null; - } - return flutterSection['plugin'] as YamlMap?; -} diff --git a/script/tool/lib/src/common/pub_version_finder.dart b/script/tool/lib/src/common/pub_version_finder.dart deleted file mode 100644 index c24ec429f8a3..000000000000 --- a/script/tool/lib/src/common/pub_version_finder.dart +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; - -import 'package:http/http.dart' as http; -import 'package:pub_semver/pub_semver.dart'; - -/// Finding version of [package] that is published on pub. -class PubVersionFinder { - /// Constructor. - /// - /// Note: you should manually close the [httpClient] when done using the finder. - PubVersionFinder({this.pubHost = defaultPubHost, required this.httpClient}); - - /// The default pub host to use. - static const String defaultPubHost = 'https://pub.dev'; - - /// The pub host url, defaults to `https://pub.dev`. - final String pubHost; - - /// The http client. - /// - /// You should manually close this client when done using this finder. - final http.Client httpClient; - - /// Get the package version on pub. - Future getPackageVersion( - {required String packageName}) async { - assert(packageName.isNotEmpty); - final Uri pubHostUri = Uri.parse(pubHost); - final Uri url = pubHostUri.replace(path: '/packages/$packageName.json'); - final http.Response response = await httpClient.get(url); - - if (response.statusCode == 404) { - return PubVersionFinderResponse( - versions: [], - result: PubVersionFinderResult.noPackageFound, - httpResponse: response); - } else if (response.statusCode != 200) { - return PubVersionFinderResponse( - versions: [], - result: PubVersionFinderResult.fail, - httpResponse: response); - } - final Map responseBody = - json.decode(response.body) as Map; - final List versions = (responseBody['versions']! as List) - .cast() - .map( - (final String versionString) => Version.parse(versionString)) - .toList(); - - return PubVersionFinderResponse( - versions: versions, - result: PubVersionFinderResult.success, - httpResponse: response); - } -} - -/// Represents a response for [PubVersionFinder]. -class PubVersionFinderResponse { - /// Constructor. - PubVersionFinderResponse( - {required this.versions, - required this.result, - required this.httpResponse}) { - if (versions.isNotEmpty) { - versions.sort((Version a, Version b) { - // TODO(cyanglaz): Think about how to handle pre-release version with [Version.prioritize]. - // https://github.com/flutter/flutter/issues/82222 - return b.compareTo(a); - }); - } - } - - /// The versions found in [PubVersionFinder]. - /// - /// This is sorted by largest to smallest, so the first element in the list is the largest version. - /// Might be `null` if the [result] is not [PubVersionFinderResult.success]. - final List versions; - - /// The result of the version finder. - final PubVersionFinderResult result; - - /// The response object of the http request. - final http.Response httpResponse; -} - -/// An enum representing the result of [PubVersionFinder]. -enum PubVersionFinderResult { - /// The version finder successfully found a version. - success, - - /// The version finder failed to find a valid version. - /// - /// This might due to http connection errors or user errors. - fail, - - /// The version finder failed to locate the package. - /// - /// This indicates the package is new. - noPackageFound, -} diff --git a/script/tool/lib/src/common/xcode.dart b/script/tool/lib/src/common/xcode.dart deleted file mode 100644 index 83f681bcb492..000000000000 --- a/script/tool/lib/src/common/xcode.dart +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; - -import 'core.dart'; -import 'process_runner.dart'; - -const String _xcodeBuildCommand = 'xcodebuild'; -const String _xcRunCommand = 'xcrun'; - -/// A utility class for interacting with the installed version of Xcode. -class Xcode { - /// Creates an instance that runs commands with the given [processRunner]. - /// - /// If [log] is true, commands run by this instance will long various status - /// messages. - Xcode({ - this.processRunner = const ProcessRunner(), - this.log = false, - }); - - /// The [ProcessRunner] used to run commands. Overridable for testing. - final ProcessRunner processRunner; - - /// Whether or not to log when running commands. - final bool log; - - /// Runs an `xcodebuild` in [directory] with the given parameters. - Future runXcodeBuild( - Directory directory, { - List actions = const ['build'], - required String workspace, - required String scheme, - String? configuration, - List extraFlags = const [], - }) { - final List args = [ - _xcodeBuildCommand, - ...actions, - if (workspace != null) ...['-workspace', workspace], - if (scheme != null) ...['-scheme', scheme], - if (configuration != null) ...['-configuration', configuration], - ...extraFlags, - ]; - final String completeTestCommand = '$_xcRunCommand ${args.join(' ')}'; - if (log) { - print(completeTestCommand); - } - return processRunner.runAndStream(_xcRunCommand, args, - workingDir: directory); - } - - /// Returns true if [project], which should be an .xcodeproj directory, - /// contains a target called [target], false if it does not, and null if the - /// check fails (e.g., if [project] is not an Xcode project). - Future projectHasTarget(Directory project, String target) async { - final io.ProcessResult result = - await processRunner.run(_xcRunCommand, [ - _xcodeBuildCommand, - '-list', - '-json', - '-project', - project.path, - ]); - if (result.exitCode != 0) { - return null; - } - Map? projectInfo; - try { - projectInfo = (jsonDecode(result.stdout as String) - as Map)['project'] as Map?; - } on FormatException { - return null; - } - if (projectInfo == null) { - return null; - } - final List? targets = - (projectInfo['targets'] as List?)?.cast(); - return targets?.contains(target) ?? false; - } - - /// Returns the newest available simulator (highest OS version, with ties - /// broken in favor of newest device), if any. - Future findBestAvailableIphoneSimulator() async { - final List findSimulatorsArguments = [ - 'simctl', - 'list', - 'devices', - 'runtimes', - 'available', - '--json', - ]; - final String findSimulatorCompleteCommand = - '$_xcRunCommand ${findSimulatorsArguments.join(' ')}'; - if (log) { - print('Looking for available simulators...'); - print(findSimulatorCompleteCommand); - } - final io.ProcessResult findSimulatorsResult = - await processRunner.run(_xcRunCommand, findSimulatorsArguments); - if (findSimulatorsResult.exitCode != 0) { - if (log) { - printError( - 'Error occurred while running "$findSimulatorCompleteCommand":\n' - '${findSimulatorsResult.stderr}'); - } - return null; - } - final Map simulatorListJson = - jsonDecode(findSimulatorsResult.stdout as String) - as Map; - final List> runtimes = - (simulatorListJson['runtimes'] as List) - .cast>(); - final Map devices = - (simulatorListJson['devices'] as Map) - .cast(); - if (runtimes.isEmpty || devices.isEmpty) { - return null; - } - String? id; - // Looking for runtimes, trying to find one with highest OS version. - for (final Map rawRuntimeMap in runtimes.reversed) { - final Map runtimeMap = - rawRuntimeMap.cast(); - if ((runtimeMap['name'] as String?)?.contains('iOS') != true) { - continue; - } - final String? runtimeID = runtimeMap['identifier'] as String?; - if (runtimeID == null) { - continue; - } - final List>? devicesForRuntime = - (devices[runtimeID] as List?)?.cast>(); - if (devicesForRuntime == null || devicesForRuntime.isEmpty) { - continue; - } - // Looking for runtimes, trying to find latest version of device. - for (final Map rawDevice in devicesForRuntime.reversed) { - final Map device = rawDevice.cast(); - id = device['udid'] as String?; - if (id == null) { - continue; - } - if (log) { - print('device selected: $device'); - } - return id; - } - } - return null; - } -} diff --git a/script/tool/lib/src/create_all_packages_app_command.dart b/script/tool/lib/src/create_all_packages_app_command.dart deleted file mode 100644 index e7719e9f664c..000000000000 --- a/script/tool/lib/src/create_all_packages_app_command.dart +++ /dev/null @@ -1,344 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:pubspec_parse/pubspec_parse.dart'; - -import 'common/core.dart'; -import 'common/package_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -const String _outputDirectoryFlag = 'output-dir'; - -const String _projectName = 'all_packages'; - -const int _exitUpdateMacosPodfileFailed = 3; -const int _exitUpdateMacosPbxprojFailed = 4; -const int _exitGenNativeBuildFilesFailed = 5; - -/// A command to create an application that builds all in a single application. -class CreateAllPackagesAppCommand extends PackageCommand { - /// Creates an instance of the builder command. - CreateAllPackagesAppCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Directory? pluginsRoot, - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - final Directory defaultDir = - pluginsRoot ?? packagesDir.fileSystem.currentDirectory; - argParser.addOption(_outputDirectoryFlag, - defaultsTo: defaultDir.path, - help: - 'The path the directory to create the "$_projectName" project in.\n' - 'Defaults to the repository root.'); - } - - /// The location to create the synthesized app project. - Directory get _appDirectory => packagesDir.fileSystem - .directory(getStringArg(_outputDirectoryFlag)) - .childDirectory(_projectName); - - /// The synthesized app project. - RepositoryPackage get app => RepositoryPackage(_appDirectory); - - @override - String get description => - 'Generate Flutter app that includes all target packagas.'; - - @override - String get name => 'create-all-packages-app'; - - @override - Future run() async { - final int exitCode = await _createApp(); - if (exitCode != 0) { - throw ToolExit(exitCode); - } - - final Set excluded = getExcludedPackageNames(); - if (excluded.isNotEmpty) { - print('Exluding the following plugins from the combined build:'); - for (final String plugin in excluded) { - print(' $plugin'); - } - print(''); - } - - await _genPubspecWithAllPlugins(); - - // Run `flutter pub get` to generate all native build files. - // TODO(stuartmorgan): This hangs on Windows for some reason. Since it's - // currently not needed on Windows, skip it there, but we should investigate - // further and/or implement https://github.com/flutter/flutter/issues/93407, - // and remove the need for this conditional. - if (!platform.isWindows) { - if (!await _genNativeBuildFiles()) { - printError( - "Failed to generate native build files via 'flutter pub get'"); - throw ToolExit(_exitGenNativeBuildFilesFailed); - } - } - - await Future.wait(>[ - _updateAppGradle(), - _updateManifest(), - _updateMacosPbxproj(), - // This step requires the native file generation triggered by - // flutter pub get above, so can't currently be run on Windows. - if (!platform.isWindows) _updateMacosPodfile(), - ]); - } - - Future _createApp() async { - final io.ProcessResult result = io.Process.runSync( - flutterCommand, - [ - 'create', - '--template=app', - '--project-name=$_projectName', - '--android-language=java', - _appDirectory.path, - ], - ); - - print(result.stdout); - print(result.stderr); - return result.exitCode; - } - - Future _updateAppGradle() async { - final File gradleFile = app - .platformDirectory(FlutterPlatform.android) - .childDirectory('app') - .childFile('build.gradle'); - if (!gradleFile.existsSync()) { - throw ToolExit(64); - } - - final StringBuffer newGradle = StringBuffer(); - for (final String line in gradleFile.readAsLinesSync()) { - if (line.contains('minSdkVersion')) { - // minSdkVersion 20 is required by Google maps. - // minSdkVersion 19 is required by WebView. - newGradle.writeln('minSdkVersion 20'); - } else if (line.contains('compileSdkVersion')) { - // compileSdkVersion 32 is required by webview_flutter. - newGradle.writeln('compileSdkVersion 32'); - } else { - newGradle.writeln(line); - } - if (line.contains('defaultConfig {')) { - newGradle.writeln(' multiDexEnabled true'); - } else if (line.contains('dependencies {')) { - // 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 = app - .platformDirectory(FlutterPlatform.android) - .childDirectory('app') - .childDirectory('src') - .childDirectory('main') - .childFile('AndroidManifest.xml'); - if (!manifestFile.existsSync()) { - throw ToolExit(64); - } - - final StringBuffer newManifest = StringBuffer(); - for (final String line in manifestFile.readAsLinesSync()) { - if (line.contains('package="com.example.$_projectName"')) { - newManifest - ..writeln('package="com.example.$_projectName"') - ..writeln('xmlns:tools="http://schemas.android.com/tools">') - ..writeln() - ..writeln( - '', - ); - } else { - newManifest.writeln(line); - } - } - manifestFile.writeAsStringSync(newManifest.toString()); - } - - Future _genPubspecWithAllPlugins() async { - // Read the old pubspec file's Dart SDK version, in order to preserve it - // in the new file. The template sometimes relies on having opted in to - // specific language features via SDK version, so using a different one - // can cause compilation failures. - final Pubspec originalPubspec = app.parsePubspec(); - const String dartSdkKey = 'sdk'; - final VersionConstraint dartSdkConstraint = - originalPubspec.environment?[dartSdkKey] ?? - VersionConstraint.compatibleWith( - Version.parse('2.12.0'), - ); - - final Map pluginDeps = - await _getValidPathDependencies(); - final Pubspec pubspec = Pubspec( - _projectName, - description: 'Flutter app containing all 1st party plugins.', - version: Version.parse('1.0.0+1'), - environment: { - dartSdkKey: dartSdkConstraint, - }, - dependencies: { - 'flutter': SdkDependency('flutter'), - }..addAll(pluginDeps), - devDependencies: { - 'flutter_test': SdkDependency('flutter'), - }, - dependencyOverrides: pluginDeps, - ); - app.pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); - } - - Future> _getValidPathDependencies() async { - final Map pathDependencies = - {}; - - await for (final PackageEnumerationEntry entry in getTargetPackages()) { - final RepositoryPackage package = entry.package; - final Directory pluginDirectory = package.directory; - final String pluginName = pluginDirectory.basename; - final Pubspec pubspec = package.parsePubspec(); - - if (pubspec.publishTo != 'none') { - pathDependencies[pluginName] = PathDependency(pluginDirectory.path); - } - } - return pathDependencies; - } - - String _pubspecToString(Pubspec pubspec) { - return ''' -### Generated file. Do not edit. Run `dart pub global run flutter_plugin_tools gen-pubspec` to update. -name: ${pubspec.name} -description: ${pubspec.description} -publish_to: none - -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 (final MapEntry entry in values.entries) { - buffer.writeln(); - final Object? entryValue = entry.value; - if (entryValue is VersionConstraint) { - String value = entryValue.toString(); - // Range constraints require quoting. - if (value.startsWith('>') || value.startsWith('<')) { - value = "'$value'"; - } - buffer.write(' ${entry.key}: $value'); - } else if (entryValue is SdkDependency) { - buffer.write(' ${entry.key}: \n sdk: ${entryValue.sdk}'); - } else if (entryValue is PathDependency) { - String depPath = entryValue.path; - if (path.style == p.Style.windows) { - // Posix-style path separators are preferred in pubspec.yaml (and - // using a consistent format makes unit testing simpler), so convert. - final List components = path.split(depPath); - final String firstComponent = components.first; - // path.split leaves a \ on drive components that isn't necessary, - // and confuses pub, so remove it. - if (firstComponent.endsWith(r':\')) { - components[0] = - firstComponent.substring(0, firstComponent.length - 1); - } - depPath = p.posix.joinAll(components); - } - buffer.write(' ${entry.key}: \n path: $depPath'); - } else { - throw UnimplementedError( - 'Not available for type: ${entryValue.runtimeType}', - ); - } - } - - return buffer.toString(); - } - - Future _genNativeBuildFiles() async { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - ['pub', 'get'], - workingDir: _appDirectory, - ); - return exitCode == 0; - } - - Future _updateMacosPodfile() async { - /// Only change the macOS deployment target if the host platform is macOS. - /// The Podfile is not generated on other platforms. - if (!platform.isMacOS) { - return; - } - - final File podfileFile = - app.platformDirectory(FlutterPlatform.macos).childFile('Podfile'); - if (!podfileFile.existsSync()) { - printError("Can't find Podfile for macOS"); - throw ToolExit(_exitUpdateMacosPodfileFailed); - } - - final StringBuffer newPodfile = StringBuffer(); - for (final String line in podfileFile.readAsLinesSync()) { - if (line.contains('platform :osx')) { - // macOS 10.15 is required by in_app_purchase. - newPodfile.writeln("platform :osx, '10.15'"); - } else { - newPodfile.writeln(line); - } - } - podfileFile.writeAsStringSync(newPodfile.toString()); - } - - Future _updateMacosPbxproj() async { - final File pbxprojFile = app - .platformDirectory(FlutterPlatform.macos) - .childDirectory('Runner.xcodeproj') - .childFile('project.pbxproj'); - if (!pbxprojFile.existsSync()) { - printError("Can't find project.pbxproj for macOS"); - throw ToolExit(_exitUpdateMacosPbxprojFailed); - } - - final StringBuffer newPbxproj = StringBuffer(); - for (final String line in pbxprojFile.readAsLinesSync()) { - if (line.contains('MACOSX_DEPLOYMENT_TARGET')) { - // macOS 10.15 is required by in_app_purchase. - newPbxproj.writeln(' MACOSX_DEPLOYMENT_TARGET = 10.15;'); - } else { - newPbxproj.writeln(line); - } - } - pbxprojFile.writeAsStringSync(newPbxproj.toString()); - } -} diff --git a/script/tool/lib/src/custom_test_command.dart b/script/tool/lib/src/custom_test_command.dart deleted file mode 100644 index 0ef6e602c070..000000000000 --- a/script/tool/lib/src/custom_test_command.dart +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -const String _scriptName = 'run_tests.dart'; -const String _legacyScriptName = 'run_tests.sh'; - -/// A command to run custom, package-local tests on packages. -/// -/// This is an escape hatch for adding tests that this tooling doesn't support. -/// It should be used sparingly; prefer instead to add functionality to this -/// tooling to eliminate the need for bespoke tests. -class CustomTestCommand extends PackageLoopingCommand { - /// Creates a custom test command instance. - CustomTestCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform); - - @override - final String name = 'custom-test'; - - @override - final String description = 'Runs package-specific custom tests defined in ' - "a package's tool/$_scriptName file.\n\n" - 'This command requires "dart" to be in your path.'; - - @override - Future runForPackage(RepositoryPackage package) async { - final File script = - package.directory.childDirectory('tool').childFile(_scriptName); - final File legacyScript = package.directory.childFile(_legacyScriptName); - String? customSkipReason; - bool ranTests = false; - - // Run the custom Dart script if presest. - if (script.existsSync()) { - // Ensure that dependencies are available. - final int pubGetExitCode = await processRunner.runAndStream( - 'dart', ['pub', 'get'], - workingDir: package.directory); - if (pubGetExitCode != 0) { - return PackageResult.fail( - ['Unable to get script dependencies']); - } - - final int testExitCode = await processRunner.runAndStream( - 'dart', ['run', 'tool/$_scriptName'], - workingDir: package.directory); - if (testExitCode != 0) { - return PackageResult.fail(); - } - ranTests = true; - } - - // Run the legacy script if present. - if (legacyScript.existsSync()) { - if (platform.isWindows) { - customSkipReason = '$_legacyScriptName is not supported on Windows. ' - 'Please migrate to $_scriptName.'; - } else { - final int exitCode = await processRunner.runAndStream( - legacyScript.path, [], - workingDir: package.directory); - if (exitCode != 0) { - return PackageResult.fail(); - } - ranTests = true; - } - } - - if (!ranTests) { - return PackageResult.skip(customSkipReason ?? 'No custom tests'); - } - - return PackageResult.success(); - } -} diff --git a/script/tool/lib/src/dependabot_check_command.dart b/script/tool/lib/src/dependabot_check_command.dart deleted file mode 100644 index 77b44e11b59e..000000000000 --- a/script/tool/lib/src/dependabot_check_command.dart +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:yaml/yaml.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/repository_package.dart'; - -/// A command to verify Dependabot configuration coverage of packages. -class DependabotCheckCommand extends PackageLoopingCommand { - /// Creates Dependabot check command instance. - DependabotCheckCommand(Directory packagesDir, {GitDir? gitDir}) - : super(packagesDir, gitDir: gitDir) { - argParser.addOption(_configPathFlag, - help: 'Path to the Dependabot configuration file', - defaultsTo: '.github/dependabot.yml'); - } - - static const String _configPathFlag = 'config'; - - late Directory _repoRoot; - - // The set of directories covered by "gradle" entries in the config. - Set _gradleDirs = const {}; - - @override - final String name = 'dependabot-check'; - - @override - final String description = - 'Checks that all packages have Dependabot coverage.'; - - @override - final PackageLoopingType packageLoopingType = - PackageLoopingType.includeAllSubpackages; - - @override - final bool hasLongOutput = false; - - @override - Future initializeRun() async { - _repoRoot = packagesDir.fileSystem.directory((await gitDir).path); - - final YamlMap config = loadYaml(_repoRoot - .childFile(getStringArg(_configPathFlag)) - .readAsStringSync()) as YamlMap; - final dynamic entries = config['updates']; - if (entries is! YamlList) { - return; - } - - const String typeKey = 'package-ecosystem'; - const String dirKey = 'directory'; - _gradleDirs = entries - .cast() - .where((YamlMap entry) => entry[typeKey] == 'gradle') - .map((YamlMap entry) => entry[dirKey] as String) - .toSet(); - } - - @override - Future runForPackage(RepositoryPackage package) async { - bool skipped = true; - final List errors = []; - - final RunState gradleState = _validateDependabotGradleCoverage(package); - skipped = skipped && gradleState == RunState.skipped; - if (gradleState == RunState.failed) { - printError('${indentation}Missing Gradle coverage.'); - errors.add('Missing Gradle coverage'); - } - - // TODO(stuartmorgan): Add other ecosystem checks here as more are enabled. - - if (skipped) { - return PackageResult.skip('No supported package ecosystems'); - } - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - /// Returns the state for the Dependabot coverage of the Gradle ecosystem for - /// [package]: - /// - succeeded if it includes gradle and is covered. - /// - failed if it includes gradle and is not covered. - /// - skipped if it doesn't include gradle. - RunState _validateDependabotGradleCoverage(RepositoryPackage package) { - final Directory androidDir = - package.platformDirectory(FlutterPlatform.android); - final Directory appDir = androidDir.childDirectory('app'); - if (appDir.existsSync()) { - // It's an app, so only check for the app directory to be covered. - final String dependabotPath = - '/${getRelativePosixPath(appDir, from: _repoRoot)}'; - return _gradleDirs.contains(dependabotPath) - ? RunState.succeeded - : RunState.failed; - } else if (androidDir.existsSync()) { - // It's a library, so only check for the android directory to be covered. - final String dependabotPath = - '/${getRelativePosixPath(androidDir, from: _repoRoot)}'; - return _gradleDirs.contains(dependabotPath) - ? RunState.succeeded - : RunState.failed; - } - return RunState.skipped; - } -} diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart deleted file mode 100644 index e8fb11b5f289..000000000000 --- a/script/tool/lib/src/drive_examples_command.dart +++ /dev/null @@ -1,380 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io'; - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -const int _exitNoPlatformFlags = 2; -const int _exitNoAvailableDevice = 3; - -/// A command to run the example applications for packages via Flutter driver. -class DriveExamplesCommand extends PackageLoopingCommand { - /// Creates an instance of the drive command. - DriveExamplesCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag(platformAndroid, - help: 'Runs the Android implementation of the examples'); - argParser.addFlag(platformIOS, - help: 'Runs the iOS implementation of the examples'); - argParser.addFlag(platformLinux, - help: 'Runs the Linux implementation of the examples'); - argParser.addFlag(platformMacOS, - help: 'Runs the macOS implementation of the examples'); - argParser.addFlag(platformWeb, - help: 'Runs the web implementation of the examples'); - argParser.addFlag(platformWindows, - help: 'Runs the Windows 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 package example apps.\n\n' - 'For each *_test.dart in test_driver/ it drives an application with ' - 'either the corresponding test in test_driver (for example, ' - 'test_driver/app_test.dart would match test_driver/app.dart), or the ' - '*_test.dart files in integration_test/.\n\n' - 'This command requires "flutter" to be in your path.'; - - Map> _targetDeviceFlags = const >{}; - - @override - Future initializeRun() async { - final List platformSwitches = [ - platformAndroid, - platformIOS, - platformLinux, - platformMacOS, - platformWeb, - platformWindows, - ]; - final int platformCount = platformSwitches - .where((String platform) => getBoolArg(platform)) - .length; - // The flutter tool currently doesn't accept multiple device arguments: - // https://github.com/flutter/flutter/issues/35733 - // If that is implemented, this check can be relaxed. - if (platformCount != 1) { - printError( - 'Exactly one of ${platformSwitches.map((String platform) => '--$platform').join(', ')} ' - 'must be specified.'); - throw ToolExit(_exitNoPlatformFlags); - } - - String? androidDevice; - if (getBoolArg(platformAndroid)) { - final List devices = await _getDevicesForPlatform('android'); - if (devices.isEmpty) { - printError('No Android devices available'); - throw ToolExit(_exitNoAvailableDevice); - } - androidDevice = devices.first; - } - - String? iOSDevice; - if (getBoolArg(platformIOS)) { - final List devices = await _getDevicesForPlatform('ios'); - if (devices.isEmpty) { - printError('No iOS devices available'); - throw ToolExit(_exitNoAvailableDevice); - } - iOSDevice = devices.first; - } - - _targetDeviceFlags = >{ - if (getBoolArg(platformAndroid)) - platformAndroid: ['-d', androidDevice!], - if (getBoolArg(platformIOS)) platformIOS: ['-d', iOSDevice!], - if (getBoolArg(platformLinux)) platformLinux: ['-d', 'linux'], - if (getBoolArg(platformMacOS)) platformMacOS: ['-d', 'macos'], - if (getBoolArg(platformWeb)) - platformWeb: [ - '-d', - 'web-server', - '--web-port=7357', - '--browser-name=chrome', - if (platform.environment.containsKey('CHROME_EXECUTABLE')) - '--chrome-binary=${platform.environment['CHROME_EXECUTABLE']}', - ], - if (getBoolArg(platformWindows)) - platformWindows: ['-d', 'windows'], - }; - } - - @override - Future runForPackage(RepositoryPackage package) async { - final bool isPlugin = isFlutterPlugin(package); - - if (package.isPlatformInterface && package.getExamples().isEmpty) { - // Platform interface packages generally aren't intended to have - // examples, and don't need integration tests, so skip rather than fail. - return PackageResult.skip( - 'Platform interfaces are not expected to have integration tests.'); - } - - // For plugin packages, skip if the plugin itself doesn't support any - // requested platform(s). - if (isPlugin) { - final Iterable requestedPlatforms = _targetDeviceFlags.keys; - final Iterable unsupportedPlatforms = requestedPlatforms.where( - (String platform) => !pluginSupportsPlatform(platform, package)); - for (final String platform in unsupportedPlatforms) { - print('Skipping unsupported platform $platform...'); - } - if (unsupportedPlatforms.length == requestedPlatforms.length) { - return PackageResult.skip( - '${package.displayName} does not support any requested platform.'); - } - } - - int examplesFound = 0; - int supportedExamplesFound = 0; - bool testsRan = false; - final List errors = []; - for (final RepositoryPackage example in package.getExamples()) { - ++examplesFound; - final String exampleName = - getRelativePosixPath(example.directory, from: packagesDir); - - // Skip examples that don't support any requested platform(s). - final List deviceFlags = _deviceFlagsForExample(example); - if (deviceFlags.isEmpty) { - print( - 'Skipping $exampleName; does not support any requested platforms.'); - continue; - } - ++supportedExamplesFound; - - final List drivers = await _getDrivers(example); - if (drivers.isEmpty) { - print('No driver tests found for $exampleName'); - continue; - } - - for (final File driver in drivers) { - final List testTargets = []; - - // Try to find a matching app to drive without the _test.dart - // TODO(stuartmorgan): Migrate all remaining uses of this legacy - // approach (currently only video_player) and remove support for it: - // https://github.com/flutter/flutter/issues/85224. - final File? legacyTestFile = _getLegacyTestFileForTestDriver(driver); - if (legacyTestFile != null) { - testTargets.add(legacyTestFile); - } else { - for (final File testFile in await _getIntegrationTests(example)) { - // Check files for known problematic patterns. - final bool passesValidation = _validateIntegrationTest(testFile); - if (!passesValidation) { - // Report the issue, but continue with the test as the validation - // errors don't prevent running. - errors.add('${testFile.basename} failed validation'); - } - testTargets.add(testFile); - } - } - - if (testTargets.isEmpty) { - final String driverRelativePath = - getRelativePosixPath(driver, from: package.directory); - printError( - 'Found $driverRelativePath, but no integration_test/*_test.dart files.'); - errors.add('No test files for $driverRelativePath'); - continue; - } - - testsRan = true; - final List failingTargets = await _driveTests( - example, driver, testTargets, - deviceFlags: deviceFlags); - for (final File failingTarget in failingTargets) { - errors.add( - getRelativePosixPath(failingTarget, from: package.directory)); - } - } - } - if (!testsRan) { - // It is an error for a plugin not to have integration tests, because that - // is the only way to test the method channel communication. - if (isPlugin) { - printError( - 'No driver tests were run ($examplesFound example(s) found).'); - errors.add('No tests ran (use --exclude if this is intentional).'); - } else { - return PackageResult.skip(supportedExamplesFound == 0 - ? 'No example supports requested platform(s).' - : 'No example is configured for driver tests.'); - } - } - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - /// Returns the device flags for the intersection of the requested platforms - /// and the platforms supported by [example]. - List _deviceFlagsForExample(RepositoryPackage example) { - final List deviceFlags = []; - for (final MapEntry> entry - in _targetDeviceFlags.entries) { - final String platform = entry.key; - if (example.directory.childDirectory(platform).existsSync()) { - deviceFlags.addAll(entry.value); - } else { - final String exampleName = - getRelativePosixPath(example.directory, from: packagesDir); - print('Skipping unsupported platform $platform for $exampleName'); - } - } - return deviceFlags; - } - - Future> _getDevicesForPlatform(String platform) async { - final List deviceIds = []; - - final ProcessResult result = await processRunner.run( - flutterCommand, ['devices', '--machine'], - stdoutEncoding: utf8); - if (result.exitCode != 0) { - return deviceIds; - } - - String output = result.stdout as String; - // --machine doesn't currently prevent the tool from printing banners; - // see https://github.com/flutter/flutter/issues/86055. This workaround - // can be removed once that is fixed. - output = output.substring(output.indexOf('[')); - - final List> devices = - (jsonDecode(output) as List).cast>(); - for (final Map deviceInfo in devices) { - final String targetPlatform = - (deviceInfo['targetPlatform'] as String?) ?? ''; - if (targetPlatform.startsWith(platform)) { - final String? deviceId = deviceInfo['id'] as String?; - if (deviceId != null) { - deviceIds.add(deviceId); - } - } - } - return deviceIds; - } - - Future> _getDrivers(RepositoryPackage example) async { - final List drivers = []; - - final Directory driverDir = example.directory.childDirectory('test_driver'); - if (driverDir.existsSync()) { - await for (final FileSystemEntity driver in driverDir.list()) { - if (driver is File && driver.basename.endsWith('_test.dart')) { - drivers.add(driver); - } - } - } - return drivers; - } - - File? _getLegacyTestFileForTestDriver(File testDriver) { - final String testName = testDriver.basename.replaceAll( - RegExp(r'_test.dart$'), - '.dart', - ); - final File testFile = testDriver.parent.childFile(testName); - - return testFile.existsSync() ? testFile : null; - } - - Future> _getIntegrationTests(RepositoryPackage example) async { - final List tests = []; - final Directory integrationTestDir = - example.directory.childDirectory('integration_test'); - - if (integrationTestDir.existsSync()) { - await for (final FileSystemEntity file in integrationTestDir.list()) { - if (file is File && file.basename.endsWith('_test.dart')) { - tests.add(file); - } - } - } - return tests; - } - - /// Checks [testFile] for known bad patterns in integration tests, logging - /// any issues. - /// - /// Returns true if the file passes validation without issues. - bool _validateIntegrationTest(File testFile) { - final List lines = testFile.readAsLinesSync(); - - final RegExp badTestPattern = RegExp(r'\s*test\('); - if (lines.any((String line) => line.startsWith(badTestPattern))) { - final String filename = testFile.basename; - printError( - '$filename uses "test", which will not report failures correctly. ' - 'Use testWidgets instead.'); - return false; - } - - return true; - } - - /// For each file in [targets], uses - /// `flutter drive --driver [driver] --target ` - /// to drive [example], returning a list of any failing test targets. - /// - /// [deviceFlags] should contain the flags to run the test on a specific - /// target device (plus any supporting device-specific flags). E.g.: - /// - `['-d', 'macos']` for driving for macOS. - /// - `['-d', 'web-server', '--web-port=', '--browser-name=]` - /// for web - Future> _driveTests( - RepositoryPackage example, - File driver, - List targets, { - required List deviceFlags, - }) async { - final List failures = []; - - final String enableExperiment = getStringArg(kEnableExperiment); - - for (final File target in targets) { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'drive', - ...deviceFlags, - if (enableExperiment.isNotEmpty) - '--enable-experiment=$enableExperiment', - '--driver', - getRelativePosixPath(driver, from: example.directory), - '--target', - getRelativePosixPath(target, from: example.directory), - ], - workingDir: example.directory); - if (exitCode != 0) { - failures.add(target); - } - } - return failures; - } -} diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart deleted file mode 100644 index 93a832eb0e29..000000000000 --- a/script/tool/lib/src/federation_safety_check_command.dart +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; - -import 'common/core.dart'; -import 'common/file_utils.dart'; -import 'common/git_version_finder.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// A command to check that PRs don't violate repository best practices that -/// have been established to avoid breakages that building and testing won't -/// catch. -class FederationSafetyCheckCommand extends PackageLoopingCommand { - /// Creates an instance of the safety check command. - FederationSafetyCheckCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - GitDir? gitDir, - }) : super( - packagesDir, - processRunner: processRunner, - platform: platform, - gitDir: gitDir, - ); - - // A map of package name (as defined by the directory name of the package) - // to a list of changed Dart files in that package, as Posix paths relative to - // the package root. - // - // This only considers top-level packages, not subpackages such as example/. - final Map> _changedDartFiles = >{}; - - // The set of *_platform_interface packages that will have public code changes - // published. - final Set _modifiedAndPublishedPlatformInterfacePackages = {}; - - // The set of conceptual plugins (not packages) that have changes. - final Set _changedPlugins = {}; - - static const String _platformInterfaceSuffix = '_platform_interface'; - - @override - final String name = 'federation-safety-check'; - - @override - final String description = - 'Checks that the change does not violate repository rules around changes ' - 'to federated plugin packages.'; - - @override - bool get hasLongOutput => false; - - @override - Future initializeRun() async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final String baseSha = await gitVersionFinder.getBaseSha(); - print('Validating changes relative to "$baseSha"\n'); - for (final String path in await gitVersionFinder.getChangedFiles()) { - // Git output always uses Posix paths. - final List allComponents = p.posix.split(path); - final int packageIndex = allComponents.indexOf('packages'); - if (packageIndex == -1) { - continue; - } - final List relativeComponents = - allComponents.sublist(packageIndex + 1); - // The package name is either the directory directly under packages/, or - // the directory under that in the case of a federated plugin. - String packageName = relativeComponents.removeAt(0); - // Count the top-level plugin as changed. - _changedPlugins.add(packageName); - if (relativeComponents[0] == packageName || - (relativeComponents.length > 1 && - relativeComponents[0].startsWith('${packageName}_'))) { - packageName = relativeComponents.removeAt(0); - } - - if (relativeComponents.last.endsWith('.dart')) { - _changedDartFiles[packageName] ??= []; - _changedDartFiles[packageName]! - .add(p.posix.joinAll(relativeComponents)); - } - - if (packageName.endsWith(_platformInterfaceSuffix) && - relativeComponents.first == 'pubspec.yaml' && - await _packageWillBePublished(path)) { - _modifiedAndPublishedPlatformInterfacePackages.add(packageName); - } - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - if (!isFlutterPlugin(package)) { - return PackageResult.skip('Not a plugin.'); - } - - if (!package.isFederated) { - return PackageResult.skip('Not a federated plugin.'); - } - - if (package.isPlatformInterface) { - // As the leaf nodes in the graph, a published package interface change is - // assumed to be correct, and other changes are validated against that. - return PackageResult.skip( - 'Platform interface changes are not validated.'); - } - - // Uses basename to match _changedPackageFiles. - final String basePackageName = package.directory.parent.basename; - final String platformInterfacePackageName = - '$basePackageName$_platformInterfaceSuffix'; - final List changedPlatformInterfaceFiles = - _changedDartFiles[platformInterfacePackageName] ?? []; - - if (!_modifiedAndPublishedPlatformInterfacePackages - .contains(platformInterfacePackageName)) { - print('No published changes for $platformInterfacePackageName.'); - return PackageResult.success(); - } - - if (!changedPlatformInterfaceFiles - .any((String path) => path.startsWith('lib/'))) { - print('No public code changes for $platformInterfacePackageName.'); - return PackageResult.success(); - } - - final List changedPackageFiles = - _changedDartFiles[package.directory.basename] ?? []; - if (changedPackageFiles.isEmpty) { - print('No Dart changes.'); - return PackageResult.success(); - } - - // If the change would be flagged, but it appears to be a mass change - // rather than a plugin-specific change, allow it with a warning. - // - // This is a tradeoff between safety and convenience; forcing mass changes - // to be split apart is not ideal, and the assumption is that reviewers are - // unlikely to accidentally approve a PR that is supposed to be changing a - // single plugin, but touches other plugins (vs accidentally approving a - // PR that changes multiple parts of a single plugin, which is a relatively - // easy mistake to make). - // - // 3 is chosen to minimize the chances of accidentally letting something - // through (vs 2, which could be a single-plugin change with one stray - // change to another file accidentally included), while not setting too - // high a bar for detecting mass changes. This can be tuned if there are - // issues with false positives or false negatives. - const int massChangePluginThreshold = 3; - if (_changedPlugins.length >= massChangePluginThreshold) { - logWarning('Ignoring potentially dangerous change, as this appears ' - 'to be a mass change.'); - return PackageResult.success(); - } - - printError('Dart changes are not allowed to other packages in ' - '$basePackageName in the same PR as changes to public Dart code in ' - '$platformInterfacePackageName, as this can cause accidental breaking ' - 'changes to be missed by automated checks. Please split the changes to ' - 'these two packages into separate PRs.\n\n' - 'If you believe that this is a false positive, please file a bug.'); - return PackageResult.fail( - ['$platformInterfacePackageName changed.']); - } - - Future _packageWillBePublished( - String pubspecRepoRelativePosixPath) async { - final File pubspecFile = childFileWithSubcomponents( - packagesDir.parent, p.posix.split(pubspecRepoRelativePosixPath)); - if (!pubspecFile.existsSync()) { - // If the package was deleted, nothing will be published. - return false; - } - final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); - if (pubspec.publishTo == 'none') { - return false; - } - - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final Version? previousVersion = - await gitVersionFinder.getPackageVersion(pubspecRepoRelativePosixPath); - if (previousVersion == null) { - // The plugin is new, so it will be published. - return true; - } - return pubspec.version != previousVersion; - } -} diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart deleted file mode 100644 index a11284411908..000000000000 --- a/script/tool/lib/src/firebase_test_lab_command.dart +++ /dev/null @@ -1,358 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; -import 'package:uuid/uuid.dart'; - -import 'common/core.dart'; -import 'common/gradle.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -const int _exitGcloudAuthFailed = 2; - -/// A command to run tests via Firebase test lab. -class FirebaseTestLabCommand extends PackageLoopingCommand { - /// Creates an instance of the test runner command. - FirebaseTestLabCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addOption( - 'project', - defaultsTo: 'flutter-cirrus', - help: 'The Firebase project name.', - ); - final String? homeDir = io.Platform.environment['HOME']; - argParser.addOption('service-key', - defaultsTo: homeDir == null - ? null - : path.join(homeDir, 'gcloud-service-key.json'), - help: 'The path to the service key for gcloud authentication.\n' - r'If not provided, \$HOME/gcloud-service-key.json will be ' - r'assumed if $HOME is set.'); - argParser.addOption('test-run-id', - defaultsTo: const 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.addOption('build-id', - defaultsTo: - io.Platform.environment['CIRRUS_BUILD_ID'] ?? 'unknown_build', - help: - 'Optional string to append to the results path, to avoid conflicts. ' - r'Defaults to $CIRRUS_BUILD_ID if that is set.'); - argParser.addMultiOption('device', - splitCommas: false, - defaultsTo: [ - 'model=walleye,version=26', - 'model=redfin,version=30' - ], - 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_cirrus_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.'; - - bool _firebaseProjectConfigured = false; - - Future _configureFirebaseProject() async { - if (_firebaseProjectConfigured) { - return; - } - - final String serviceKey = getStringArg('service-key'); - if (serviceKey.isEmpty) { - print('No --service-key provided; skipping gcloud authorization'); - } else { - final io.ProcessResult result = await processRunner.run( - 'gcloud', - [ - 'auth', - 'activate-service-account', - '--key-file=$serviceKey', - ], - logOnError: true, - ); - if (result.exitCode != 0) { - printError('Unable to activate gcloud account.'); - throw ToolExit(_exitGcloudAuthFailed); - } - final int exitCode = await processRunner.runAndStream('gcloud', [ - 'config', - 'set', - 'project', - getStringArg('project'), - ]); - print(''); - if (exitCode == 0) { - print('Firebase project configured.'); - } else { - logWarning( - 'Warning: gcloud config set returned a non-zero exit code. Continuing anyway.'); - } - } - _firebaseProjectConfigured = true; - } - - @override - Future runForPackage(RepositoryPackage package) async { - final List results = []; - for (final RepositoryPackage example in package.getExamples()) { - results.add(await _runForExample(example, package: package)); - } - - // If all results skipped, report skip overall. - if (results - .every((PackageResult result) => result.state == RunState.skipped)) { - return PackageResult.skip('No examples support Android.'); - } - // Otherwise, report failure if there were any failures. - final List allErrors = results - .map((PackageResult result) => - result.state == RunState.failed ? result.details : []) - .expand((List list) => list) - .toList(); - return allErrors.isEmpty - ? PackageResult.success() - : PackageResult.fail(allErrors); - } - - /// Runs the test for the given example of [package]. - Future _runForExample( - RepositoryPackage example, { - required RepositoryPackage package, - }) async { - final Directory androidDirectory = - example.platformDirectory(FlutterPlatform.android); - if (!androidDirectory.existsSync()) { - return PackageResult.skip( - '${example.displayName} does not support Android.'); - } - - final Directory uiTestDirectory = androidDirectory - .childDirectory('app') - .childDirectory('src') - .childDirectory('androidTest'); - if (!uiTestDirectory.existsSync()) { - printError('No androidTest directory found.'); - return PackageResult.fail( - ['No tests ran (use --exclude if this is intentional).']); - } - - // Ensure that the Dart integration tests will be run, not just native UI - // tests. - if (!await _testsContainDartIntegrationTestRunner(uiTestDirectory)) { - printError('No integration_test runner found. ' - 'See the integration_test package README for setup instructions.'); - return PackageResult.fail(['No integration_test runner.']); - } - - // Ensures that gradle wrapper exists - final GradleProject project = GradleProject(example, - processRunner: processRunner, platform: platform); - if (!await _ensureGradleWrapperExists(project)) { - return PackageResult.fail(['Unable to build example apk']); - } - - await _configureFirebaseProject(); - - if (!await _runGradle(project, 'app:assembleAndroidTest')) { - return PackageResult.fail(['Unable to assemble androidTest']); - } - - final List errors = []; - - // Used within the loop to ensure a unique GCS output location for each - // test file's run. - int resultsCounter = 0; - for (final File test in _findIntegrationTestFiles(example)) { - final String testName = - getRelativePosixPath(test, from: package.directory); - print('Testing $testName...'); - if (!await _runGradle(project, 'app:assembleDebug', testFile: test)) { - printError('Could not build $testName'); - errors.add('$testName failed to build'); - continue; - } - final String buildId = getStringArg('build-id'); - final String testRunId = getStringArg('test-run-id'); - final String resultsDir = - 'plugins_android_test/${package.displayName}/$buildId/$testRunId/' - '${example.directory.basename}/${resultsCounter++}/'; - - // Automatically retry failures; there is significant flake with these - // tests whose cause isn't yet understood, and having to re-run the - // entire shard for a flake in any one test is extremely slow. This should - // be removed once the root cause of the flake is understood. - // See https://github.com/flutter/flutter/issues/95063 - const int maxRetries = 2; - bool passing = false; - for (int i = 1; i <= maxRetries && !passing; ++i) { - if (i > 1) { - logWarning('$testName failed on attempt ${i - 1}. Retrying...'); - } - passing = await _runFirebaseTest(example, test, resultsDir: resultsDir); - } - if (!passing) { - printError('Test failure for $testName after $maxRetries attempts'); - errors.add('$testName failed tests'); - } - } - - if (errors.isEmpty && resultsCounter == 0) { - printError('No integration tests were run.'); - errors.add('No tests ran (use --exclude if this is intentional).'); - } - - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - /// Checks that Gradle has been configured for [project], and if not runs a - /// Flutter build to generate it. - /// - /// Returns true if either gradlew was already present, or the build succeeds. - Future _ensureGradleWrapperExists(GradleProject project) async { - if (!project.isConfigured()) { - print('Running flutter build apk...'); - final String experiment = getStringArg(kEnableExperiment); - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'build', - 'apk', - if (experiment.isNotEmpty) '--enable-experiment=$experiment', - ], - workingDir: project.androidDirectory); - - if (exitCode != 0) { - return false; - } - } - return true; - } - - /// Runs [test] from [example] as a Firebase Test Lab test, returning true if - /// the test passed. - /// - /// [resultsDir] should be a unique-to-the-test-run directory to store the - /// results on the server. - Future _runFirebaseTest( - RepositoryPackage example, - File test, { - required String resultsDir, - }) async { - 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', - '7m', - '--results-bucket=${getStringArg('results-bucket')}', - '--results-dir=$resultsDir', - for (final String device in getStringListArg('device')) ...[ - '--device', - device - ], - ]; - final int exitCode = await processRunner.runAndStream('gcloud', args, - workingDir: example.directory); - - return exitCode == 0; - } - - /// Builds [target] using Gradle in the given [project]. Assumes Gradle is - /// already configured. - /// - /// [testFile] optionally does the Flutter build with the given test file as - /// the build target. - /// - /// Returns true if the command succeeds. - Future _runGradle( - GradleProject project, - String target, { - File? testFile, - }) async { - final String experiment = getStringArg(kEnableExperiment); - final String? extraOptions = experiment.isNotEmpty - ? Uri.encodeComponent('--enable-experiment=$experiment') - : null; - - final int exitCode = await project.runCommand( - target, - arguments: [ - '-Pverbose=true', - if (testFile != null) '-Ptarget=${testFile.path}', - if (extraOptions != null) '-Pextra-front-end-options=$extraOptions', - if (extraOptions != null) '-Pextra-gen-snapshot-options=$extraOptions', - ], - ); - - if (exitCode != 0) { - return false; - } - return true; - } - - /// Finds and returns all integration test files for [example]. - Iterable _findIntegrationTestFiles(RepositoryPackage example) sync* { - final Directory integrationTestDir = - example.directory.childDirectory('integration_test'); - - if (!integrationTestDir.existsSync()) { - return; - } - - yield* integrationTestDir - .listSync(recursive: true) - .where((FileSystemEntity file) => - file is File && file.basename.endsWith('_test.dart')) - .cast(); - } - - /// Returns true if any of the test files in [uiTestDirectory] contain the - /// annotation that means that the test will reports the results of running - /// the Dart integration tests. - Future _testsContainDartIntegrationTestRunner( - Directory uiTestDirectory) async { - return uiTestDirectory - .list(recursive: true, followLinks: false) - .where((FileSystemEntity entity) => entity is File) - .cast() - .any((File file) { - return file.basename.endsWith('.java') && - file.readAsStringSync().contains('@RunWith(FlutterTestRunner.class)'); - }); - } -} diff --git a/script/tool/lib/src/fix_command.dart b/script/tool/lib/src/fix_command.dart deleted file mode 100644 index 2819609eabbd..000000000000 --- a/script/tool/lib/src/fix_command.dart +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// A command to run Dart's "fix" command on packages. -class FixCommand extends PackageLoopingCommand { - /// Creates a fix command instance. - FixCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform); - - @override - final String name = 'fix'; - - @override - final String description = 'Fixes packages using dart fix.\n\n' - 'This command requires "dart" and "flutter" to be in your path, and ' - 'assumes that dependencies have already been fetched (e.g., by running ' - 'the analyze command first).'; - - @override - final bool hasLongOutput = false; - - @override - PackageLoopingType get packageLoopingType => - PackageLoopingType.includeAllSubpackages; - - @override - Future runForPackage(RepositoryPackage package) async { - final int exitCode = await processRunner.runAndStream( - 'dart', ['fix', '--apply'], - workingDir: package.directory); - if (exitCode != 0) { - printError('Unable to automatically fix package.'); - return PackageResult.fail(); - } - return PackageResult.success(); - } -} diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart deleted file mode 100644 index e4236878658c..000000000000 --- a/script/tool/lib/src/format_command.dart +++ /dev/null @@ -1,369 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_command.dart'; -import 'common/process_runner.dart'; - -/// In theory this should be 8191, but in practice that was still resulting in -/// "The input line is too long" errors. This was chosen as a value that worked -/// in practice in testing with flutter/plugins, but may need to be adjusted -/// based on further experience. -@visibleForTesting -const int windowsCommandLineMax = 8000; - -/// This value is picked somewhat arbitrarily based on checking `ARG_MAX` on a -/// macOS and Linux machine. If anyone encounters a lower limit in pratice, it -/// can be lowered accordingly. -@visibleForTesting -const int nonWindowsCommandLineMax = 1000000; - -const int _exitClangFormatFailed = 3; -const int _exitFlutterFormatFailed = 4; -const int _exitJavaFormatFailed = 5; -const int _exitGitFailed = 6; -const int _exitDependencyMissing = 7; - -final Uri _googleFormatterUrl = Uri.https('github.com', - '/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'); - -/// A command to format all package code. -class FormatCommand extends PackageCommand { - /// Creates an instance of the format command. - FormatCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag('fail-on-change', hide: true); - argParser.addOption('clang-format', - defaultsTo: 'clang-format', help: 'Path to "clang-format" executable.'); - argParser.addOption('java', - defaultsTo: 'java', help: 'Path to "java" executable.'); - } - - @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 { - final String googleFormatterPath = await _getGoogleFormatterPath(); - - // This class is not based on PackageLoopingCommand because running the - // formatters separately for each package is an order of magnitude slower, - // due to the startup overhead of the formatters. - final Iterable files = - await _getFilteredFilePaths(getFiles(), relativeTo: packagesDir); - await _formatDart(files); - await _formatJava(files, googleFormatterPath); - await _formatCppAndObjectiveC(files); - - if (getBoolArg('fail-on-change')) { - final bool modified = await _didModifyAnything(); - if (modified) { - throw ToolExit(exitCommandFoundErrors); - } - } - } - - Future _didModifyAnything() async { - final io.ProcessResult modifiedFiles = await processRunner.run( - 'git', - ['ls-files', '--modified'], - workingDir: packagesDir, - logOnError: true, - ); - if (modifiedFiles.exitCode != 0) { - printError('Unable to determine changed files.'); - throw ToolExit(_exitGitFailed); - } - - print('\n\n'); - - final String stdout = modifiedFiles.stdout as String; - if (stdout.isEmpty) { - print('All files formatted correctly.'); - return false; - } - - print('These files are not formatted correctly (see diff below):'); - LineSplitter.split(stdout).map((String line) => ' $line').forEach(print); - - print('\nTo fix run "dart pub global activate flutter_plugin_tools && ' - 'dart pub global run flutter_plugin_tools format" or copy-paste ' - 'this command into your terminal:'); - - final io.ProcessResult diff = await processRunner.run( - 'git', - ['diff'], - workingDir: packagesDir, - logOnError: true, - ); - if (diff.exitCode != 0) { - printError('Unable to determine diff.'); - throw ToolExit(_exitGitFailed); - } - print('patch -p1 < _formatCppAndObjectiveC(Iterable files) async { - final Iterable clangFiles = _getPathsWithExtensions( - files, {'.h', '.m', '.mm', '.cc', '.cpp'}); - if (clangFiles.isNotEmpty) { - final String clangFormat = await _findValidClangFormat(); - - print('Formatting .cc, .cpp, .h, .m, and .mm files...'); - final int exitCode = await _runBatched( - clangFormat, ['-i', '--style=file'], - files: clangFiles); - if (exitCode != 0) { - printError( - 'Failed to format C, C++, and Objective-C files: exit code $exitCode.'); - throw ToolExit(_exitClangFormatFailed); - } - } - } - - Future _findValidClangFormat() async { - final String clangFormatArg = getStringArg('clang-format'); - if (await _hasDependency(clangFormatArg)) { - return clangFormatArg; - } - - // There is a known issue where "chromium/depot_tools/clang-format" - // fails with "Problem while looking for clang-format in Chromium source tree". - // Loop through all "clang-format"s in PATH until a working one is found, - // for example "/usr/local/bin/clang-format" or a "brew" installed version. - for (final String clangFormatPath in await _whichAll('clang-format')) { - if (await _hasDependency(clangFormatPath)) { - return clangFormatPath; - } - } - printError('Unable to run "clang-format". Make sure that it is in your ' - 'path, or provide a full path with --clang-format.'); - throw ToolExit(_exitDependencyMissing); - } - - Future _formatJava( - Iterable files, String googleFormatterPath) async { - final Iterable javaFiles = - _getPathsWithExtensions(files, {'.java'}); - if (javaFiles.isNotEmpty) { - final String java = getStringArg('java'); - if (!await _hasDependency(java)) { - printError( - 'Unable to run "java". Make sure that it is in your path, or ' - 'provide a full path with --java.'); - throw ToolExit(_exitDependencyMissing); - } - - print('Formatting .java files...'); - final int exitCode = await _runBatched( - java, ['-jar', googleFormatterPath, '--replace'], - files: javaFiles); - if (exitCode != 0) { - printError('Failed to format Java files: exit code $exitCode.'); - throw ToolExit(_exitJavaFormatFailed); - } - } - } - - Future _formatDart(Iterable files) async { - final Iterable dartFiles = - _getPathsWithExtensions(files, {'.dart'}); - if (dartFiles.isNotEmpty) { - print('Formatting .dart files...'); - final int exitCode = - await _runBatched('dart', ['format'], files: dartFiles); - if (exitCode != 0) { - printError('Failed to format Dart files: exit code $exitCode.'); - throw ToolExit(_exitFlutterFormatFailed); - } - } - } - - /// Given a stream of [files], returns the paths of any that are not in known - /// locations to ignore, relative to [relativeTo]. - Future> _getFilteredFilePaths( - Stream files, { - required Directory relativeTo, - }) async { - // Returns a pattern to check for [directories] as a subset of a file path. - RegExp pathFragmentForDirectories(List directories) { - String s = path.separator; - // Escape the separator for use in the regex. - if (s == r'\') { - s = r'\\'; - } - return RegExp('(?:^|$s)${path.joinAll(directories)}$s'); - } - - final String fromPath = relativeTo.path; - - // Dart files are allowed to have a pragma to disable auto-formatting. This - // was added because Hixie hurts when dealing with what dartfmt does to - // artisanally-formatted Dart, while Stuart gets really frustrated when - // dealing with PRs from newer contributors who don't know how to make Dart - // readable. After much discussion, it was decided that files in the plugins - // and packages repos that really benefit from hand-formatting (e.g. files - // with large blobs of hex literals) could be opted-out of the requirement - // that they be autoformatted, so long as the code's owner was willing to - // bear the cost of this during code reviews. - // In the event that code ownership moves to someone who does not hold the - // same views as the original owner, the pragma can be removed and the file - // auto-formatted. - const String handFormattedExtension = '.dart'; - const String handFormattedPragma = '// This file is hand-formatted.'; - - return files - .where((File file) { - // See comment above near [handFormattedPragma]. - return path.extension(file.path) != handFormattedExtension || - !file.readAsLinesSync().contains(handFormattedPragma); - }) - .map((File file) => path.relative(file.path, from: fromPath)) - .where((String path) => - // Ignore files in build/ directories (e.g., headers of frameworks) - // to avoid useless extra work in local repositories. - !path.contains( - pathFragmentForDirectories(['example', 'build'])) && - // Ignore files in Pods, which are not part of the repository. - !path.contains(pathFragmentForDirectories(['Pods'])) && - // Ignore .dart_tool/, which can have various intermediate files. - !path.contains(pathFragmentForDirectories(['.dart_tool']))) - .toList(); - } - - Iterable _getPathsWithExtensions( - Iterable files, Set extensions) { - return files.where( - (String filePath) => extensions.contains(path.extension(filePath))); - } - - Future _getGoogleFormatterPath() async { - final String javaFormatterPath = path.join( - path.dirname(path.fromUri(platform.script)), - 'google-java-format-1.3-all-deps.jar'); - final File javaFormatterFile = - packagesDir.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; - } - - /// Returns true if [command] can be run successfully. - Future _hasDependency(String command) async { - // Some versions of Java accept both -version and --version, but some only - // accept -version. - final String versionFlag = command == 'java' ? '-version' : '--version'; - try { - final io.ProcessResult result = - await processRunner.run(command, [versionFlag]); - if (result.exitCode != 0) { - return false; - } - } on io.ProcessException { - // Thrown when the binary is missing entirely. - return false; - } - return true; - } - - /// Returns all instances of [command] executable found on user path. - Future> _whichAll(String command) async { - try { - final io.ProcessResult result = - await processRunner.run('which', ['-a', command]); - - if (result.exitCode != 0) { - return []; - } - - final String stdout = (result.stdout as String).trim(); - if (stdout.isEmpty) { - return []; - } - return LineSplitter.split(stdout).toList(); - } on io.ProcessException { - return []; - } - } - - /// Runs [command] on [arguments] on all of the files in [files], batched as - /// necessary to avoid OS command-line length limits. - /// - /// Returns the exit code of the first failure, which stops the run, or 0 - /// on success. - Future _runBatched( - String command, - List arguments, { - required Iterable files, - }) async { - final int commandLineMax = - platform.isWindows ? windowsCommandLineMax : nonWindowsCommandLineMax; - - // Compute the max length of the file argument portion of a batch. - // Add one to each argument's length for the space before it. - final int argumentTotalLength = - arguments.fold(0, (int sum, String arg) => sum + arg.length + 1); - final int batchMaxTotalLength = - commandLineMax - command.length - argumentTotalLength; - - // Run the command in batches. - final List> batches = - _partitionFileList(files, maxStringLength: batchMaxTotalLength); - for (final List batch in batches) { - batch.sort(); // For ease of testing. - final int exitCode = await processRunner.runAndStream( - command, [...arguments, ...batch], - workingDir: packagesDir); - if (exitCode != 0) { - return exitCode; - } - } - return 0; - } - - /// Partitions [files] into batches whose max string length as parameters to - /// a command (including the spaces between them, and between the list and - /// the command itself) is no longer than [maxStringLength]. - List> _partitionFileList(Iterable files, - {required int maxStringLength}) { - final List> batches = >[[]]; - int currentBatchTotalLength = 0; - for (final String file in files) { - final int length = file.length + 1 /* for the space */; - if (currentBatchTotalLength + length > maxStringLength) { - // Start a new batch. - batches.add([]); - currentBatchTotalLength = 0; - } - batches.last.add(file); - currentBatchTotalLength += length; - } - return batches; - } -} diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart deleted file mode 100644 index 0517bcf43298..000000000000 --- a/script/tool/lib/src/license_check_command.dart +++ /dev/null @@ -1,308 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_command.dart'; - -const Set _codeFileExtensions = { - '.c', - '.cc', - '.cpp', - '.dart', - '.h', - '.html', - '.java', - '.kt', - '.m', - '.mm', - '.swift', - '.sh', -}; - -// Basenames without extensions of files to ignore. -const Set _ignoreBasenameList = { - 'flutter_export_environment', - 'GeneratedPluginRegistrant', - 'generated_plugin_registrant', -}; - -// File suffixes that otherwise match _codeFileExtensions to ignore. -const Set _ignoreSuffixList = { - '.g.dart', // Generated API code. - '.mocks.dart', // Generated by Mockito. -}; - -// Full basenames of files to ignore. -const Set _ignoredFullBasenameList = { - 'resource.h', // Generated by VS. -}; - -// Copyright and license regexes for third-party code. -// -// These are intentionally very simple, since there is very little third-party -// code in this repository. Complexity can be added as-needed on a case-by-case -// basis. -// -// When adding license regexes here, include the copyright info to ensure that -// any new additions are flagged for added scrutiny in review. -final List _thirdPartyLicenseBlockRegexes = [ - // Third-party code used in url_launcher_web. - RegExp( - r'^// Copyright 2017 Workiva Inc\..*' - r'^// Licensed under the Apache License, Version 2\.0', - multiLine: true, - dotAll: true, - ), - // Third-party code used in google_maps_flutter_web. - RegExp( - r'^// The MIT License [^C]+ Copyright \(c\) 2008 Krasimir Tsonev', - multiLine: true, - ), - // bsdiff in flutter/packages. - RegExp( - r'// Copyright 2003-2005 Colin Percival\. All rights reserved\.\n' - r'// Use of this source code is governed by a BSD-style license that can be\n' - r'// found in the LICENSE file\.\n', - ), -]; - -// The exact format of the BSD license that our license files should contain. -// Slight variants are not accepted because they may prevent consolidation in -// tools that assemble all licenses used in distributed applications. -// standardized. -const String _fullBsdLicenseText = ''' -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -'''; - -/// Validates that code files have copyright and license blocks. -class LicenseCheckCommand extends PackageCommand { - /// Creates a new license check command for [packagesDir]. - LicenseCheckCommand(Directory packagesDir, - {Platform platform = const LocalPlatform(), GitDir? gitDir}) - : super(packagesDir, platform: platform, gitDir: gitDir); - - @override - final String name = 'license-check'; - - @override - final String description = - 'Ensures that all code files have copyright/license blocks.'; - - @override - Future run() async { - // Create a set of absolute paths to submodule directories, with trailing - // separator, to do prefix matching with to test directory inclusion. - final Iterable submodulePaths = (await _getSubmoduleDirectories()) - .map( - (Directory dir) => '${dir.absolute.path}${platform.pathSeparator}'); - - final Iterable allFiles = (await _getAllFiles()).where( - (File file) => !submodulePaths.any(file.absolute.path.startsWith)); - - final Iterable codeFiles = allFiles.where((File file) => - _codeFileExtensions.contains(p.extension(file.path)) && - !_shouldIgnoreFile(file)); - final Iterable firstPartyLicenseFiles = allFiles.where((File file) => - path.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); - - final List licenseFileFailures = - await _checkLicenseFiles(firstPartyLicenseFiles); - final Map<_LicenseFailureType, List> codeFileFailures = - await _checkCodeLicenses(codeFiles); - - bool passed = true; - - print('\n=======================================\n'); - - if (licenseFileFailures.isNotEmpty) { - passed = false; - printError( - 'The following LICENSE files do not follow the expected format:'); - for (final File file in licenseFileFailures) { - printError(' ${file.path}'); - } - printError('Please ensure that they use the exact format used in this ' - 'repository".\n'); - } - - if (codeFileFailures[_LicenseFailureType.incorrectFirstParty]!.isNotEmpty) { - passed = false; - printError('The license block for these files is missing or incorrect:'); - for (final File file - in codeFileFailures[_LicenseFailureType.incorrectFirstParty]!) { - printError(' ${file.path}'); - } - printError( - 'If this third-party code, move it to a "third_party/" directory, ' - 'otherwise ensure that you are using the exact copyright and license ' - 'text used by all first-party files in this repository.\n'); - } - - if (codeFileFailures[_LicenseFailureType.unknownThirdParty]!.isNotEmpty) { - passed = false; - printError( - 'No recognized license was found for the following third-party files:'); - for (final File file - in codeFileFailures[_LicenseFailureType.unknownThirdParty]!) { - printError(' ${file.path}'); - } - print('Please check that they have a license at the top of the file. ' - 'If they do, the license check needs to be updated to recognize ' - 'the new third-party license block.\n'); - } - - if (!passed) { - throw ToolExit(1); - } - - printSuccess('All files passed validation!'); - } - - // Creates the expected copyright+license block for first-party code. - String _generateLicenseBlock( - String comment, { - String prefix = '', - String suffix = '', - }) { - return '$prefix${comment}Copyright 2013 The Flutter Authors. All rights reserved.\n' - '${comment}Use of this source code is governed by a BSD-style license that can be\n' - '${comment}found in the LICENSE file.$suffix\n'; - } - - /// Checks all license blocks for [codeFiles], returning any that fail - /// validation. - Future>> _checkCodeLicenses( - Iterable codeFiles) async { - final List incorrectFirstPartyFiles = []; - final List unrecognizedThirdPartyFiles = []; - - // Most code file types in the repository use '//' comments. - final String defaultFirstParyLicenseBlock = _generateLicenseBlock('// '); - // A few file types have a different comment structure. - final Map firstPartyLicenseBlockByExtension = - { - '.sh': _generateLicenseBlock('# '), - '.html': _generateLicenseBlock('', prefix: ''), - }; - - for (final File file in codeFiles) { - print('Checking ${file.path}'); - // On Windows, git may auto-convert line endings on checkout; this should - // still pass since they will be converted back on commit. - final String content = - (await file.readAsString()).replaceAll('\r\n', '\n'); - - final String firstParyLicense = - firstPartyLicenseBlockByExtension[p.extension(file.path)] ?? - defaultFirstParyLicenseBlock; - if (_isThirdParty(file)) { - // Third-party directories allow either known third-party licenses, our - // the first-party license, as there may be local additions. - if (!_thirdPartyLicenseBlockRegexes - .any((RegExp regex) => regex.hasMatch(content)) && - !content.contains(firstParyLicense)) { - unrecognizedThirdPartyFiles.add(file); - } - } else { - if (!content.contains(firstParyLicense)) { - incorrectFirstPartyFiles.add(file); - } - } - } - - // Sort by path for more usable output. - int pathCompare(File a, File b) => a.path.compareTo(b.path); - incorrectFirstPartyFiles.sort(pathCompare); - unrecognizedThirdPartyFiles.sort(pathCompare); - - return <_LicenseFailureType, List>{ - _LicenseFailureType.incorrectFirstParty: incorrectFirstPartyFiles, - _LicenseFailureType.unknownThirdParty: unrecognizedThirdPartyFiles, - }; - } - - /// Checks all provided LICENSE [files], returning any that fail validation. - Future> _checkLicenseFiles(Iterable files) async { - final List incorrectLicenseFiles = []; - - for (final File file in files) { - print('Checking ${file.path}'); - // On Windows, git may auto-convert line endings on checkout; this should - // still pass since they will be converted back on commit. - final String contents = file.readAsStringSync().replaceAll('\r\n', '\n'); - if (!contents.contains(_fullBsdLicenseText)) { - incorrectLicenseFiles.add(file); - } - } - - return incorrectLicenseFiles; - } - - bool _shouldIgnoreFile(File file) { - final String path = file.path; - return _ignoreBasenameList.contains(p.basenameWithoutExtension(path)) || - _ignoreSuffixList.any((String suffix) => - path.endsWith(suffix) || - _ignoredFullBasenameList.contains(p.basename(path))); - } - - bool _isThirdParty(File file) { - return path.split(file.path).contains('third_party'); - } - - Future> _getAllFiles() => packagesDir.parent - .list(recursive: true, followLinks: false) - .where((FileSystemEntity entity) => entity is File) - .map((FileSystemEntity file) => file as File) - .toList(); - - // Returns the directories containing mapped submodules, if any. - Future> _getSubmoduleDirectories() async { - final List submodulePaths = []; - final Directory repoRoot = - packagesDir.fileSystem.directory((await gitDir).path); - final File submoduleSpec = repoRoot.childFile('.gitmodules'); - if (submoduleSpec.existsSync()) { - final RegExp pathLine = RegExp(r'path\s*=\s*(.*)'); - for (final String line in submoduleSpec.readAsLinesSync()) { - final RegExpMatch? match = pathLine.firstMatch(line); - if (match != null) { - submodulePaths.add(repoRoot.childDirectory(match.group(1)!.trim())); - } - } - } - return submodulePaths; - } -} - -enum _LicenseFailureType { incorrectFirstParty, unknownThirdParty } diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart deleted file mode 100644 index eb78ce891685..000000000000 --- a/script/tool/lib/src/lint_android_command.dart +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/gradle.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// Run 'gradlew lint'. -/// -/// See https://developer.android.com/studio/write/lint. -class LintAndroidCommand extends PackageLoopingCommand { - /// Creates an instance of the linter command. - LintAndroidCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform); - - @override - final String name = 'lint-android'; - - @override - final String description = 'Runs "gradlew lint" on Android plugins.\n\n' - 'Requires the examples to have been build at least once before running.'; - - @override - Future runForPackage(RepositoryPackage package) async { - if (!pluginSupportsPlatform(platformAndroid, package, - requiredMode: PlatformSupport.inline)) { - return PackageResult.skip( - 'Plugin does not have an Android implementation.'); - } - - bool failed = false; - for (final RepositoryPackage example in package.getExamples()) { - final GradleProject project = GradleProject(example, - processRunner: processRunner, platform: platform); - - if (!project.isConfigured()) { - return PackageResult.fail(['Build examples before linting']); - } - - final String packageName = package.directory.basename; - - // Only lint one build mode to avoid extra work. - // Only lint the plugin project itself, to avoid failing due to errors in - // dependencies. - // - // TODO(stuartmorgan): Consider adding an XML parser to read and summarize - // all results. Currently, only the first three errors will be shown - // inline, and the rest have to be checked via the CI-uploaded artifact. - final int exitCode = await project.runCommand('$packageName:lintDebug'); - if (exitCode != 0) { - failed = true; - } - } - - return failed ? PackageResult.fail() : PackageResult.success(); - } -} diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart deleted file mode 100644 index b47657e47eff..000000000000 --- a/script/tool/lib/src/list_command.dart +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/package_command.dart'; -import 'common/repository_package.dart'; - -/// A command to list different types of repository content. -class ListCommand extends PackageCommand { - /// Creates an instance of the list command, whose behavior depends on the - /// 'type' argument it provides. - ListCommand( - Directory packagesDir, { - Platform platform = const LocalPlatform(), - }) : super(packagesDir, platform: platform) { - argParser.addOption( - _type, - defaultsTo: _package, - allowed: [_package, _example, _allPackage, _file], - help: 'What type of file system content to list.', - ); - } - - static const String _type = 'type'; - static const String _allPackage = 'package-or-subpackage'; - 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 { - switch (getStringArg(_type)) { - case _package: - await for (final PackageEnumerationEntry entry in getTargetPackages()) { - print(entry.package.path); - } - break; - case _example: - final Stream examples = getTargetPackages() - .expand( - (PackageEnumerationEntry entry) => entry.package.getExamples()); - await for (final RepositoryPackage package in examples) { - print(package.path); - } - break; - case _allPackage: - await for (final PackageEnumerationEntry entry - in getTargetPackagesAndSubpackages()) { - print(entry.package.path); - } - break; - case _file: - await for (final File file in getFiles()) { - print(file.path); - } - break; - } - } -} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart index 0083e0cbb8ee..6b421ebaebc0 100644 --- a/script/tool/lib/src/main.dart +++ b/script/tool/lib/src/main.dart @@ -9,34 +9,19 @@ import 'package:file/file.dart'; import 'package:file/local.dart'; import 'analyze_command.dart'; -import 'build_examples_command.dart'; import 'common/core.dart'; -import 'create_all_packages_app_command.dart'; -import 'custom_test_command.dart'; -import 'dependabot_check_command.dart'; -import 'drive_examples_command.dart'; -import 'federation_safety_check_command.dart'; -import 'firebase_test_lab_command.dart'; -import 'fix_command.dart'; -import 'format_command.dart'; -import 'license_check_command.dart'; -import 'lint_android_command.dart'; -import 'list_command.dart'; -import 'make_deps_path_based_command.dart'; -import 'native_test_command.dart'; -import 'podspec_check_command.dart'; -import 'publish_check_command.dart'; -import 'publish_command.dart'; -import 'pubspec_check_command.dart'; -import 'readme_check_command.dart'; -import 'remove_dev_dependencies.dart'; -import 'test_command.dart'; -import 'update_excerpts_command.dart'; -import 'update_release_info_command.dart'; -import 'version_check_command.dart'; -import 'xcode_analyze_command.dart'; void main(List args) { + print(''' +*** WARNING *** +This copy of the tooling is now only here as a shim for scripts in other +repositories that have not yet been updated, and can only run 'analyze'. For +full tooling in this repository, see the updated instructions: +https://github.com/flutter/packages/blob/main/script/tool/README.md +to switch to running the published version. + +'''); + const FileSystem fileSystem = LocalFileSystem(); Directory packagesDir = @@ -54,32 +39,7 @@ void main(List args) { final CommandRunner commandRunner = CommandRunner( 'dart pub global run flutter_plugin_tools', 'Productivity utils for hosting multiple plugins within one repository.') - ..addCommand(AnalyzeCommand(packagesDir)) - ..addCommand(BuildExamplesCommand(packagesDir)) - ..addCommand(CreateAllPackagesAppCommand(packagesDir)) - ..addCommand(CustomTestCommand(packagesDir)) - ..addCommand(DependabotCheckCommand(packagesDir)) - ..addCommand(DriveExamplesCommand(packagesDir)) - ..addCommand(FederationSafetyCheckCommand(packagesDir)) - ..addCommand(FirebaseTestLabCommand(packagesDir)) - ..addCommand(FixCommand(packagesDir)) - ..addCommand(FormatCommand(packagesDir)) - ..addCommand(LicenseCheckCommand(packagesDir)) - ..addCommand(LintAndroidCommand(packagesDir)) - ..addCommand(PodspecCheckCommand(packagesDir)) - ..addCommand(ListCommand(packagesDir)) - ..addCommand(NativeTestCommand(packagesDir)) - ..addCommand(MakeDepsPathBasedCommand(packagesDir)) - ..addCommand(PublishCheckCommand(packagesDir)) - ..addCommand(PublishCommand(packagesDir)) - ..addCommand(PubspecCheckCommand(packagesDir)) - ..addCommand(ReadmeCheckCommand(packagesDir)) - ..addCommand(RemoveDevDependenciesCommand(packagesDir)) - ..addCommand(TestCommand(packagesDir)) - ..addCommand(UpdateExcerptsCommand(packagesDir)) - ..addCommand(UpdateReleaseInfoCommand(packagesDir)) - ..addCommand(VersionCheckCommand(packagesDir)) - ..addCommand(XcodeAnalyzeCommand(packagesDir)); + ..addCommand(AnalyzeCommand(packagesDir)); commandRunner.run(args).catchError((Object e) { final ToolExit toolExit = e as ToolExit; diff --git a/script/tool/lib/src/make_deps_path_based_command.dart b/script/tool/lib/src/make_deps_path_based_command.dart deleted file mode 100644 index 10abcd44ae6e..000000000000 --- a/script/tool/lib/src/make_deps_path_based_command.dart +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:path/path.dart' as p; -import 'package:pub_semver/pub_semver.dart'; - -import 'common/core.dart'; -import 'common/git_version_finder.dart'; -import 'common/package_command.dart'; -import 'common/repository_package.dart'; - -const int _exitPackageNotFound = 3; -const int _exitCannotUpdatePubspec = 4; - -enum _RewriteOutcome { changed, noChangesNeeded, alreadyChanged } - -/// Converts all dependencies on target packages to path-based dependencies. -/// -/// This is to allow for pre-publish testing of changes that could affect other -/// packages in the repository. For instance, this allows for catching cases -/// where a non-breaking change to a platform interface package of a federated -/// plugin would cause post-publish analyzer failures in another package of that -/// plugin. -class MakeDepsPathBasedCommand extends PackageCommand { - /// Creates an instance of the command to convert selected dependencies to - /// path-based. - MakeDepsPathBasedCommand( - Directory packagesDir, { - GitDir? gitDir, - }) : super(packagesDir, gitDir: gitDir) { - argParser.addMultiOption(_targetDependenciesArg, - help: - 'The names of the packages to convert to path-based dependencies.\n' - 'Ignored if --$_targetDependenciesWithNonBreakingUpdatesArg is ' - 'passed.', - valueHelp: 'some_package'); - argParser.addFlag( - _targetDependenciesWithNonBreakingUpdatesArg, - help: 'Causes all packages that have non-breaking version changes ' - 'when compared against the git base to be treated as target ' - 'packages.', - ); - } - - static const String _targetDependenciesArg = 'target-dependencies'; - static const String _targetDependenciesWithNonBreakingUpdatesArg = - 'target-dependencies-with-non-breaking-updates'; - - // The comment to add to temporary dependency overrides. - static const String _dependencyOverrideWarningComment = - '# FOR TESTING ONLY. DO NOT MERGE.'; - - @override - final String name = 'make-deps-path-based'; - - @override - final String description = - 'Converts package dependencies to path-based references.'; - - @override - Future run() async { - final Set targetDependencies = - getBoolArg(_targetDependenciesWithNonBreakingUpdatesArg) - ? await _getNonBreakingUpdatePackages() - : getStringListArg(_targetDependenciesArg).toSet(); - - if (targetDependencies.isEmpty) { - print('No target dependencies; nothing to do.'); - return; - } - print('Rewriting references to: ${targetDependencies.join(', ')}...'); - - final Map localDependencyPackages = - _findLocalPackages(targetDependencies); - - final String repoRootPath = (await gitDir).path; - for (final File pubspec in await _getAllPubspecs()) { - final String displayPath = p.posix.joinAll( - path.split(path.relative(pubspec.absolute.path, from: repoRootPath))); - final _RewriteOutcome outcome = await _addDependencyOverridesIfNecessary( - pubspec, localDependencyPackages); - switch (outcome) { - case _RewriteOutcome.changed: - print(' Modified $displayPath'); - break; - case _RewriteOutcome.alreadyChanged: - print(' Skipped $displayPath - Already rewritten'); - break; - case _RewriteOutcome.noChangesNeeded: - break; - } - } - } - - Map _findLocalPackages(Set packageNames) { - final Map targets = - {}; - for (final String packageName in packageNames) { - final Directory topLevelCandidate = - packagesDir.childDirectory(packageName); - // If packages// exists, then either that directory is the - // package, or packages/// exists and is the - // package (in the case of a federated plugin). - if (topLevelCandidate.existsSync()) { - final Directory appFacingCandidate = - topLevelCandidate.childDirectory(packageName); - targets[packageName] = RepositoryPackage(appFacingCandidate.existsSync() - ? appFacingCandidate - : topLevelCandidate); - continue; - } - // If there is no packages/ directory, then either the - // packages doesn't exist, or it is a sub-package of a federated plugin. - // If it's the latter, it will be a directory whose name is a prefix. - for (final FileSystemEntity entity in packagesDir.listSync()) { - if (entity is Directory && packageName.startsWith(entity.basename)) { - final Directory subPackageCandidate = - entity.childDirectory(packageName); - if (subPackageCandidate.existsSync()) { - targets[packageName] = RepositoryPackage(subPackageCandidate); - break; - } - } - } - - if (!targets.containsKey(packageName)) { - printError('Unable to find package "$packageName"'); - throw ToolExit(_exitPackageNotFound); - } - } - return targets; - } - - /// If [pubspecFile] has any dependencies on packages in [localDependencies], - /// adds dependency_overrides entries to redirect them to the local version - /// using path-based dependencies. - Future<_RewriteOutcome> _addDependencyOverridesIfNecessary(File pubspecFile, - Map localDependencies) async { - final String pubspecContents = pubspecFile.readAsStringSync(); - final Pubspec pubspec = Pubspec.parse(pubspecContents); - // Fail if there are any dependency overrides already, other than ones - // created by this script. If support for that is needed at some point, it - // can be added, but currently it's not and relying on that makes the logic - // here much simpler. - if (pubspec.dependencyOverrides.isNotEmpty) { - if (pubspecContents.contains(_dependencyOverrideWarningComment)) { - return _RewriteOutcome.alreadyChanged; - } - printError( - 'Packages with dependency overrides are not currently supported.'); - throw ToolExit(_exitCannotUpdatePubspec); - } - - final Iterable combinedDependencies = [ - ...pubspec.dependencies.keys, - ...pubspec.devDependencies.keys, - ]; - final List packagesToOverride = combinedDependencies - .where( - (String packageName) => localDependencies.containsKey(packageName)) - .toList(); - // Sort the combined list to avoid sort_pub_dependencies lint violations. - packagesToOverride.sort(); - if (packagesToOverride.isNotEmpty) { - final String commonBasePath = packagesDir.path; - // Find the relative path to the common base. - final int packageDepth = path - .split(path.relative(pubspecFile.parent.absolute.path, - from: commonBasePath)) - .length; - final List relativeBasePathComponents = - List.filled(packageDepth, '..'); - // This is done via strings rather than by manipulating the Pubspec and - // then re-serialiazing so that it's a localized change, rather than - // rewriting the whole file (e.g., destroying comments), which could be - // more disruptive for local use. - String newPubspecContents = ''' -$pubspecContents - -$_dependencyOverrideWarningComment -dependency_overrides: -'''; - for (final String packageName in packagesToOverride) { - // Find the relative path from the common base to the local package. - final List repoRelativePathComponents = path.split( - path.relative(localDependencies[packageName]!.path, - from: commonBasePath)); - newPubspecContents += ''' - $packageName: - path: ${p.posix.joinAll([ - ...relativeBasePathComponents, - ...repoRelativePathComponents, - ])} -'''; - } - pubspecFile.writeAsStringSync(newPubspecContents); - return _RewriteOutcome.changed; - } - return _RewriteOutcome.noChangesNeeded; - } - - /// Returns all pubspecs anywhere under the packages directory. - Future> _getAllPubspecs() => packagesDir.parent - .list(recursive: true, followLinks: false) - .where((FileSystemEntity entity) => - entity is File && p.basename(entity.path) == 'pubspec.yaml') - .map((FileSystemEntity file) => file as File) - .toList(); - - /// Returns all packages that have non-breaking published changes (i.e., a - /// minor or bugfix version change) relative to the git comparison base. - /// - /// Prints status information about what was checked for ease of auditing logs - /// in CI. - Future> _getNonBreakingUpdatePackages() async { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final String baseSha = await gitVersionFinder.getBaseSha(); - print('Finding changed packages relative to "$baseSha"...'); - - final Set changedPackages = {}; - for (final String changedPath in await gitVersionFinder.getChangedFiles()) { - // Git output always uses Posix paths. - final List allComponents = p.posix.split(changedPath); - // Only pubspec changes are potential publishing events. - if (allComponents.last != 'pubspec.yaml' || - allComponents.contains('example')) { - continue; - } - if (!allComponents.contains(packagesDir.basename)) { - print(' Skipping $changedPath; not in packages directory.'); - continue; - } - final RepositoryPackage package = - RepositoryPackage(packagesDir.fileSystem.file(changedPath).parent); - // Ignored deleted packages, as they won't be published. - if (!package.pubspecFile.existsSync()) { - final String directoryName = p.posix.joinAll(path.split(path.relative( - package.directory.absolute.path, - from: packagesDir.path))); - print(' Skipping $directoryName; deleted.'); - continue; - } - final String packageName = package.parsePubspec().name; - if (!await _hasNonBreakingVersionChange(package)) { - // Log packages that had pubspec changes but weren't included for ease - // of auditing CI. - print(' Skipping $packageName; no non-breaking version change.'); - continue; - } - changedPackages.add(packageName); - } - return changedPackages; - } - - Future _hasNonBreakingVersionChange(RepositoryPackage package) async { - final Pubspec pubspec = package.parsePubspec(); - if (pubspec.publishTo == 'none') { - return false; - } - - final String pubspecGitPath = p.posix.joinAll(path.split(path.relative( - package.pubspecFile.absolute.path, - from: (await gitDir).path))); - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final Version? previousVersion = - await gitVersionFinder.getPackageVersion(pubspecGitPath); - if (previousVersion == null) { - // The plugin is new, so nothing can be depending on it yet. - return false; - } - final Version newVersion = pubspec.version!; - if ((newVersion.major > 0 && newVersion.major != previousVersion.major) || - (newVersion.major == 0 && newVersion.minor != previousVersion.minor)) { - // Breaking changes aren't targetted since they won't be picked up - // automatically. - return false; - } - return newVersion != previousVersion; - } -} diff --git a/script/tool/lib/src/native_test_command.dart b/script/tool/lib/src/native_test_command.dart deleted file mode 100644 index af5f4df98e86..000000000000 --- a/script/tool/lib/src/native_test_command.dart +++ /dev/null @@ -1,624 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/cmake.dart'; -import 'common/core.dart'; -import 'common/gradle.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; -import 'common/xcode.dart'; - -const String _unitTestFlag = 'unit'; -const String _integrationTestFlag = 'integration'; - -const String _iOSDestinationFlag = 'ios-destination'; - -const int _exitNoIOSSimulators = 3; - -/// The command to run native tests for plugins: -/// - iOS and macOS: XCTests (XCUnitTest and XCUITest) -/// - Android: JUnit tests -/// - Windows and Linux: GoogleTest tests -class NativeTestCommand extends PackageLoopingCommand { - /// Creates an instance of the test command. - NativeTestCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : _xcode = Xcode(processRunner: processRunner, log: true), - super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addOption( - _iOSDestinationFlag, - help: 'Specify the destination when running iOS tests.\n' - 'This is passed to the `-destination` 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 destination.', - ); - argParser.addFlag(platformAndroid, help: 'Runs Android tests'); - argParser.addFlag(platformIOS, help: 'Runs iOS tests'); - argParser.addFlag(platformLinux, help: 'Runs Linux tests'); - argParser.addFlag(platformMacOS, help: 'Runs macOS tests'); - argParser.addFlag(platformWindows, help: 'Runs Windows tests'); - - // By default, both unit tests and integration tests are run, but provide - // flags to disable one or the other. - argParser.addFlag(_unitTestFlag, - help: 'Runs native unit tests', defaultsTo: true); - argParser.addFlag(_integrationTestFlag, - help: 'Runs native integration (UI) tests', defaultsTo: true); - } - - // The device destination flags for iOS tests. - List _iOSDestinationFlags = []; - - final Xcode _xcode; - - @override - final String name = 'native-test'; - - @override - final String description = ''' -Runs native unit tests and native integration tests. - -Currently supported platforms: -- Android -- iOS: requires 'xcrun' to be in your path. -- Linux (unit tests only) -- macOS: requires 'xcrun' to be in your path. -- Windows (unit tests only) - -The example app(s) must be built for all targeted platforms before running -this command. -'''; - - Map _platforms = {}; - - List _requestedPlatforms = []; - - @override - Future initializeRun() async { - _platforms = { - platformAndroid: _PlatformDetails('Android', _testAndroid), - platformIOS: _PlatformDetails('iOS', _testIOS), - platformLinux: _PlatformDetails('Linux', _testLinux), - platformMacOS: _PlatformDetails('macOS', _testMacOS), - platformWindows: _PlatformDetails('Windows', _testWindows), - }; - _requestedPlatforms = _platforms.keys - .where((String platform) => getBoolArg(platform)) - .toList(); - _requestedPlatforms.sort(); - - if (_requestedPlatforms.isEmpty) { - printError('At least one platform flag must be provided.'); - throw ToolExit(exitInvalidArguments); - } - - if (!(getBoolArg(_unitTestFlag) || getBoolArg(_integrationTestFlag))) { - printError('At least one test type must be enabled.'); - throw ToolExit(exitInvalidArguments); - } - - if (getBoolArg(platformWindows) && getBoolArg(_integrationTestFlag)) { - logWarning('This command currently only supports unit tests for Windows. ' - 'See https://github.com/flutter/flutter/issues/70233.'); - } - - if (getBoolArg(platformLinux) && getBoolArg(_integrationTestFlag)) { - logWarning('This command currently only supports unit tests for Linux. ' - 'See https://github.com/flutter/flutter/issues/70235.'); - } - - // iOS-specific run-level state. - if (_requestedPlatforms.contains('ios')) { - String destination = getStringArg(_iOSDestinationFlag); - if (destination.isEmpty) { - final String? simulatorId = - await _xcode.findBestAvailableIphoneSimulator(); - if (simulatorId == null) { - printError('Cannot find any available iOS simulators.'); - throw ToolExit(_exitNoIOSSimulators); - } - destination = 'id=$simulatorId'; - } - _iOSDestinationFlags = [ - '-destination', - destination, - ]; - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - final List testPlatforms = []; - for (final String platform in _requestedPlatforms) { - if (!pluginSupportsPlatform(platform, package, - requiredMode: PlatformSupport.inline)) { - print('No implementation for ${_platforms[platform]!.label}.'); - continue; - } - if (!pluginHasNativeCodeForPlatform(platform, package)) { - print('No native code for ${_platforms[platform]!.label}.'); - continue; - } - testPlatforms.add(platform); - } - - if (testPlatforms.isEmpty) { - return PackageResult.skip('Nothing to test for target platform(s).'); - } - - final _TestMode mode = _TestMode( - unit: getBoolArg(_unitTestFlag), - integration: getBoolArg(_integrationTestFlag), - ); - - bool ranTests = false; - bool failed = false; - final List failureMessages = []; - for (final String platform in testPlatforms) { - final _PlatformDetails platformInfo = _platforms[platform]!; - print('Running tests for ${platformInfo.label}...'); - print('----------------------------------------'); - final _PlatformResult result = - await platformInfo.testFunction(package, mode); - ranTests |= result.state != RunState.skipped; - if (result.state == RunState.failed) { - failed = true; - - final String? error = result.error; - // Only provide the failing platforms in the failure details if testing - // multiple platforms, otherwise it's just noise. - if (_requestedPlatforms.length > 1) { - failureMessages.add(error != null - ? '${platformInfo.label}: $error' - : platformInfo.label); - } else if (error != null) { - // If there's only one platform, only provide error details in the - // summary if the platform returned a message. - failureMessages.add(error); - } - } - } - - if (!ranTests) { - return PackageResult.skip('No tests found.'); - } - return failed - ? PackageResult.fail(failureMessages) - : PackageResult.success(); - } - - Future<_PlatformResult> _testAndroid( - RepositoryPackage plugin, _TestMode mode) async { - bool exampleHasUnitTests(RepositoryPackage example) { - return example - .platformDirectory(FlutterPlatform.android) - .childDirectory('app') - .childDirectory('src') - .childDirectory('test') - .existsSync() || - plugin - .platformDirectory(FlutterPlatform.android) - .childDirectory('src') - .childDirectory('test') - .existsSync(); - } - - bool exampleHasNativeIntegrationTests(RepositoryPackage example) { - final Directory integrationTestDirectory = example - .platformDirectory(FlutterPlatform.android) - .childDirectory('app') - .childDirectory('src') - .childDirectory('androidTest'); - // There are two types of integration tests that can be in the androidTest - // directory: - // - FlutterTestRunner.class tests, which bridge to Dart integration tests - // - Purely native tests - // Only the latter is supported by this command; the former will hang if - // run here because they will wait for a Dart call that will never come. - // - // This repository uses a convention of putting the former in a - // *ActivityTest.java file, so ignore that file when checking for tests. - // Also ignore DartIntegrationTest.java, which defines the annotation used - // below for filtering the former out when running tests. - // - // If those are the only files, then there are no tests to run here. - return integrationTestDirectory.existsSync() && - integrationTestDirectory - .listSync(recursive: true) - .whereType() - .any((File file) { - final String basename = file.basename; - return !basename.endsWith('ActivityTest.java') && - basename != 'DartIntegrationTest.java'; - }); - } - - final Iterable examples = plugin.getExamples(); - - bool ranUnitTests = false; - bool ranAnyTests = false; - bool failed = false; - bool hasMissingBuild = false; - for (final RepositoryPackage example in examples) { - final bool hasUnitTests = exampleHasUnitTests(example); - final bool hasIntegrationTests = - exampleHasNativeIntegrationTests(example); - - if (mode.unit && !hasUnitTests) { - _printNoExampleTestsMessage(example, 'Android unit'); - } - if (mode.integration && !hasIntegrationTests) { - _printNoExampleTestsMessage(example, 'Android integration'); - } - - final bool runUnitTests = mode.unit && hasUnitTests; - final bool runIntegrationTests = mode.integration && hasIntegrationTests; - if (!runUnitTests && !runIntegrationTests) { - continue; - } - - final String exampleName = example.displayName; - _printRunningExampleTestsMessage(example, 'Android'); - - final GradleProject project = GradleProject( - example, - processRunner: processRunner, - platform: platform, - ); - if (!project.isConfigured()) { - printError('ERROR: Run "flutter build apk" on $exampleName, or run ' - 'this tool\'s "build-examples --apk" command, ' - 'before executing tests.'); - failed = true; - hasMissingBuild = true; - continue; - } - - if (runUnitTests) { - print('Running unit tests...'); - final int exitCode = await project.runCommand('testDebugUnitTest'); - if (exitCode != 0) { - printError('$exampleName unit tests failed.'); - failed = true; - } - ranUnitTests = true; - ranAnyTests = true; - } - - if (runIntegrationTests) { - // FlutterTestRunner-based tests will hang forever if run in a normal - // app build, since they wait for a Dart call from integration_test that - // will never come. Those tests have an extra annotation to allow - // filtering them out. - const String filter = - 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; - - print('Running integration tests...'); - final int exitCode = await project.runCommand( - 'app:connectedAndroidTest', - arguments: [ - '-Pandroid.testInstrumentationRunnerArguments.$filter', - ], - ); - if (exitCode != 0) { - printError('$exampleName integration tests failed.'); - failed = true; - } - ranAnyTests = true; - } - } - - if (failed) { - return _PlatformResult(RunState.failed, - error: hasMissingBuild - ? 'Examples must be built before testing.' - : null); - } - if (!mode.integrationOnly && !ranUnitTests) { - printError('No unit tests ran. Plugins are required to have unit tests.'); - return _PlatformResult(RunState.failed, - error: 'No unit tests ran (use --exclude if this is intentional).'); - } - if (!ranAnyTests) { - return _PlatformResult(RunState.skipped); - } - return _PlatformResult(RunState.succeeded); - } - - Future<_PlatformResult> _testIOS(RepositoryPackage plugin, _TestMode mode) { - return _runXcodeTests(plugin, 'iOS', mode, - extraFlags: _iOSDestinationFlags); - } - - Future<_PlatformResult> _testMacOS(RepositoryPackage plugin, _TestMode mode) { - return _runXcodeTests(plugin, 'macOS', mode); - } - - /// Runs all applicable tests for [plugin], printing status and returning - /// the test result. - /// - /// The tests targets must be added to the Xcode project of the example app, - /// usually at "example/{ios,macos}/Runner.xcworkspace". - Future<_PlatformResult> _runXcodeTests( - RepositoryPackage plugin, - String platform, - _TestMode mode, { - List extraFlags = const [], - }) async { - String? testTarget; - const String unitTestTarget = 'RunnerTests'; - if (mode.unitOnly) { - testTarget = unitTestTarget; - } else if (mode.integrationOnly) { - testTarget = 'RunnerUITests'; - } - - bool ranUnitTests = false; - // Assume skipped until at least one test has run. - RunState overallResult = RunState.skipped; - for (final RepositoryPackage example in plugin.getExamples()) { - final String exampleName = example.displayName; - - // If running a specific target, check that. Otherwise, check if there - // are unit tests, since having no unit tests for a plugin is fatal - // (by repo policy) even if there are integration tests. - bool exampleHasUnitTests = false; - final String? targetToCheck = - testTarget ?? (mode.unit ? unitTestTarget : null); - final Directory xcodeProject = example.directory - .childDirectory(platform.toLowerCase()) - .childDirectory('Runner.xcodeproj'); - if (targetToCheck != null) { - final bool? hasTarget = - await _xcode.projectHasTarget(xcodeProject, targetToCheck); - if (hasTarget == null) { - printError('Unable to check targets for $exampleName.'); - overallResult = RunState.failed; - continue; - } else if (!hasTarget) { - print('No "$targetToCheck" target in $exampleName; skipping.'); - continue; - } else if (targetToCheck == unitTestTarget) { - exampleHasUnitTests = true; - } - } - - _printRunningExampleTestsMessage(example, platform); - final int exitCode = await _xcode.runXcodeBuild( - example.directory, - actions: ['test'], - workspace: '${platform.toLowerCase()}/Runner.xcworkspace', - scheme: 'Runner', - configuration: 'Debug', - extraFlags: [ - if (testTarget != null) '-only-testing:$testTarget', - ...extraFlags, - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - ); - - // The exit code from 'xcodebuild test' when there are no tests. - const int xcodebuildNoTestExitCode = 66; - switch (exitCode) { - case xcodebuildNoTestExitCode: - _printNoExampleTestsMessage(example, platform); - break; - case 0: - printSuccess('Successfully ran $platform xctest for $exampleName'); - // If this is the first test, assume success until something fails. - if (overallResult == RunState.skipped) { - overallResult = RunState.succeeded; - } - if (exampleHasUnitTests) { - ranUnitTests = true; - } - break; - default: - // Any failure means a failure overall. - overallResult = RunState.failed; - // If unit tests ran, note that even if they failed. - if (exampleHasUnitTests) { - ranUnitTests = true; - } - break; - } - } - - if (!mode.integrationOnly && !ranUnitTests) { - printError('No unit tests ran. Plugins are required to have unit tests.'); - // Only return a specific summary error message about the missing unit - // tests if there weren't also failures, to avoid having a misleadingly - // specific message. - if (overallResult != RunState.failed) { - return _PlatformResult(RunState.failed, - error: 'No unit tests ran (use --exclude if this is intentional).'); - } - } - - return _PlatformResult(overallResult); - } - - Future<_PlatformResult> _testWindows( - RepositoryPackage plugin, _TestMode mode) async { - if (mode.integrationOnly) { - return _PlatformResult(RunState.skipped); - } - - bool isTestBinary(File file) { - return file.basename.endsWith('_test.exe') || - file.basename.endsWith('_tests.exe'); - } - - return _runGoogleTestTests(plugin, 'Windows', 'Debug', - isTestBinary: isTestBinary); - } - - Future<_PlatformResult> _testLinux( - RepositoryPackage plugin, _TestMode mode) async { - if (mode.integrationOnly) { - return _PlatformResult(RunState.skipped); - } - - bool isTestBinary(File file) { - return file.basename.endsWith('_test') || - file.basename.endsWith('_tests'); - } - - // Since Linux uses a single-config generator, building-examples only - // generates the build files for release, so the tests have to be run in - // release mode as well. - // - // TODO(stuartmorgan): Consider adding a command to `flutter` that would - // generate build files without doing a build, and using that instead of - // relying on running build-examples. See - // https://github.com/flutter/flutter/issues/93407. - return _runGoogleTestTests(plugin, 'Linux', 'Release', - isTestBinary: isTestBinary); - } - - /// Finds every file in the [buildDirectoryName] subdirectory of [plugin]'s - /// build directory for which [isTestBinary] is true, and runs all of them, - /// returning the overall result. - /// - /// The binaries are assumed to be Google Test test binaries, thus returning - /// zero for success and non-zero for failure. - Future<_PlatformResult> _runGoogleTestTests( - RepositoryPackage plugin, - String platformName, - String buildMode, { - required bool Function(File) isTestBinary, - }) async { - final List testBinaries = []; - bool hasMissingBuild = false; - bool buildFailed = false; - for (final RepositoryPackage example in plugin.getExamples()) { - final CMakeProject project = CMakeProject(example.directory, - buildMode: buildMode, - processRunner: processRunner, - platform: platform); - if (!project.isConfigured()) { - printError('ERROR: Run "flutter build" on ${example.displayName}, ' - 'or run this tool\'s "build-examples" command, for the target ' - 'platform before executing tests.'); - hasMissingBuild = true; - continue; - } - - // By repository convention, example projects create an aggregate target - // called 'unit_tests' that builds all unit tests (usually just an alias - // for a specific test target). - final int exitCode = await project.runBuild('unit_tests'); - if (exitCode != 0) { - printError('${example.displayName} unit tests failed to build.'); - buildFailed = true; - } - - testBinaries.addAll(project.buildDirectory - .listSync(recursive: true) - .whereType() - .where(isTestBinary) - .where((File file) { - // Only run the `buildMode` build of the unit tests, to avoid running - // the same tests multiple times. - final List components = path.split(file.path); - return components.contains(buildMode) || - components.contains(buildMode.toLowerCase()); - })); - } - - if (hasMissingBuild) { - return _PlatformResult(RunState.failed, - error: 'Examples must be built before testing.'); - } - - if (buildFailed) { - return _PlatformResult(RunState.failed, - error: 'Failed to build $platformName unit tests.'); - } - - if (testBinaries.isEmpty) { - final String binaryExtension = platform.isWindows ? '.exe' : ''; - printError( - 'No test binaries found. At least one *_test(s)$binaryExtension ' - 'binary should be built by the example(s)'); - return _PlatformResult(RunState.failed, - error: 'No $platformName unit tests found'); - } - - bool passing = true; - for (final File test in testBinaries) { - print('Running ${test.basename}...'); - final int exitCode = - await processRunner.runAndStream(test.path, []); - passing &= exitCode == 0; - } - return _PlatformResult(passing ? RunState.succeeded : RunState.failed); - } - - /// Prints a standard format message indicating that [platform] tests for - /// [plugin]'s [example] are about to be run. - void _printRunningExampleTestsMessage( - RepositoryPackage example, String platform) { - print('Running $platform tests for ${example.displayName}...'); - } - - /// Prints a standard format message indicating that no tests were found for - /// [plugin]'s [example] for [platform]. - void _printNoExampleTestsMessage(RepositoryPackage example, String platform) { - print('No $platform tests found for ${example.displayName}'); - } -} - -// The type for a function that takes a plugin directory and runs its native -// tests for a specific platform. -typedef _TestFunction = Future<_PlatformResult> Function( - RepositoryPackage, _TestMode); - -/// A collection of information related to a specific platform. -class _PlatformDetails { - const _PlatformDetails( - this.label, - this.testFunction, - ); - - /// The name to use in output. - final String label; - - /// The function to call to run tests. - final _TestFunction testFunction; -} - -/// Enabled state for different test types. -class _TestMode { - const _TestMode({required this.unit, required this.integration}); - - final bool unit; - final bool integration; - - bool get integrationOnly => integration && !unit; - bool get unitOnly => unit && !integration; -} - -/// The result of running a single platform's tests. -class _PlatformResult { - _PlatformResult(this.state, {this.error}); - - /// The overall state of the platform's tests. This should be: - /// - failed if any tests failed. - /// - succeeded if at least one test ran, and all tests passed. - /// - skipped if no tests ran. - final RunState state; - - /// An optional error string to include in the summary for this platform. - /// - /// Ignored unless [state] is `failed`. - final String? error; -} diff --git a/script/tool/lib/src/podspec_check_command.dart b/script/tool/lib/src/podspec_check_command.dart deleted file mode 100644 index 4cda7210a8ef..000000000000 --- a/script/tool/lib/src/podspec_check_command.dart +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io'; - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -const int _exitUnsupportedPlatform = 2; -const int _exitPodNotInstalled = 3; - -/// Lint the CocoaPod podspecs and run unit tests. -/// -/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. -class PodspecCheckCommand extends PackageLoopingCommand { - /// Creates an instance of the linter command. - PodspecCheckCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform); - - @override - final String name = 'podspec-check'; - - @override - List get aliases => ['podspec', 'podspecs']; - - @override - final String description = - 'Runs "pod lib lint" on all iOS and macOS plugin podspecs, as well as ' - 'making sure the podspecs follow repository standards.\n\n' - 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; - - @override - Future initializeRun() async { - if (!platform.isMacOS) { - printError('This command is only supported on macOS'); - throw ToolExit(_exitUnsupportedPlatform); - } - - final ProcessResult result = await processRunner.run( - 'which', - ['pod'], - workingDir: packagesDir, - logOnError: true, - ); - if (result.exitCode != 0) { - printError('Unable to find "pod". Make sure it is in your path.'); - throw ToolExit(_exitPodNotInstalled); - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - final List errors = []; - - final List podspecs = await _podspecsToLint(package); - if (podspecs.isEmpty) { - return PackageResult.skip('No podspecs.'); - } - - for (final File podspec in podspecs) { - if (!await _lintPodspec(podspec)) { - errors.add(podspec.basename); - } - } - - if (await _hasIOSSwiftCode(package)) { - print('iOS Swift code found, checking for search paths settings...'); - for (final File podspec in podspecs) { - if (_isPodspecMissingSearchPaths(podspec)) { - const String workaroundBlock = r''' - s.xcconfig = { - 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', - 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', - } -'''; - final String path = - getRelativePosixPath(podspec, from: package.directory); - printError('$path is missing seach path configuration. Any iOS ' - 'plugin implementation that contains Swift implementation code ' - 'needs to contain the following:\n\n' - '$workaroundBlock\n' - 'For more details, see https://github.com/flutter/flutter/issues/118418.'); - errors.add(podspec.basename); - } - } - } - - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - Future> _podspecsToLint(RepositoryPackage package) async { - final List podspecs = - await getFilesForPackage(package).where((File entity) { - final String filePath = entity.path; - return path.extension(filePath) == '.podspec'; - }).toList(); - - podspecs.sort((File a, File b) => a.basename.compareTo(b.basename)); - 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 String podspecBasename = podspec.basename; - print('Linting $podspecBasename'); - - // Lint plugin as framework (use_frameworks!). - final ProcessResult frameworkResult = - await _runPodLint(podspecPath, libraryLint: true); - print(frameworkResult.stdout); - print(frameworkResult.stderr); - - // Lint plugin as library. - final ProcessResult libraryResult = - await _runPodLint(podspecPath, libraryLint: false); - print(libraryResult.stdout); - print(libraryResult.stderr); - - return frameworkResult.exitCode == 0 && libraryResult.exitCode == 0; - } - - Future _runPodLint(String podspecPath, - {required bool libraryLint}) async { - final List arguments = [ - 'lib', - 'lint', - podspecPath, - '--configuration=Debug', // Release targets unsupported arm64 simulators. Use Debug to only build against targeted x86_64 simulator devices. - '--skip-tests', - '--use-modular-headers', // Flutter sets use_modular_headers! in its templates. - if (libraryLint) '--use-libraries' - ]; - - print('Running "pod ${arguments.join(' ')}"'); - return processRunner.run('pod', arguments, - workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); - } - - /// Returns true if there is any iOS plugin implementation code written in - /// Swift. - Future _hasIOSSwiftCode(RepositoryPackage package) async { - return getFilesForPackage(package).any((File entity) { - final String relativePath = - getRelativePosixPath(entity, from: package.directory); - // Ignore example code. - if (relativePath.startsWith('example/')) { - return false; - } - final String filePath = entity.path; - return path.extension(filePath) == '.swift'; - }); - } - - /// Returns true if [podspec] could apply to iOS, but does not have the - /// workaround for search paths that makes Swift plugins build correctly in - /// Objective-C applications. See - /// https://github.com/flutter/flutter/issues/118418 for context and details. - /// - /// This does not check that the plugin has Swift code, and thus whether the - /// workaround is needed, only whether or not it is there. - bool _isPodspecMissingSearchPaths(File podspec) { - final String directory = podspec.parent.basename; - // All macOS Flutter apps are Swift, so macOS-only podspecs don't need the - // workaround. If it's anywhere other than macos/, err or the side of - // assuming it's required. - if (directory == 'macos') { - return false; - } - - // This errs on the side of being too strict, to minimize the chance of - // accidental incorrect configuration. If we ever need more flexibility - // due to a false negative we can adjust this as necessary. - final RegExp workaround = RegExp(r''' -\s*s\.(?:ios\.)?xcconfig = {[^}]* -\s*'LIBRARY_SEARCH_PATHS' => '\$\(TOOLCHAIN_DIR\)/usr/lib/swift/\$\(PLATFORM_NAME\)/ \$\(SDKROOT\)/usr/lib/swift', -\s*'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',[^}]* -\s*}''', dotAll: true); - return !workaround.hasMatch(podspec.readAsStringSync()); - } -} diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart deleted file mode 100644 index 14b240dc04c2..000000000000 --- a/script/tool/lib/src/publish_check_command.dart +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:http/http.dart' as http; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/pub_version_finder.dart'; -import 'common/repository_package.dart'; - -/// A command to check that packages are publishable via 'dart publish'. -class PublishCheckCommand extends PackageLoopingCommand { - /// Creates an instance of the publish command. - PublishCheckCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - http.Client? httpClient, - }) : _pubVersionFinder = - PubVersionFinder(httpClient: httpClient ?? http.Client()), - super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag( - _allowPrereleaseFlag, - help: 'Allows the pre-release SDK warning to pass.\n' - 'When enabled, a pub warning, which asks to publish the package as a pre-release version when ' - 'the SDK constraint is a pre-release version, is ignored.', - ); - argParser.addFlag(_machineFlag, - help: 'Switch outputs to a machine readable JSON. \n' - 'The JSON contains a "status" field indicating the final status of the command, the possible values are:\n' - ' $_statusNeedsPublish: There is at least one package need to be published. They also passed all publish checks.\n' - ' $_statusMessageNoPublish: There are no packages needs to be published. Either no pubspec change detected or all versions have already been published.\n' - ' $_statusMessageError: Some error has occurred.'); - } - - static const String _allowPrereleaseFlag = 'allow-pre-release'; - static const String _machineFlag = 'machine'; - static const String _statusNeedsPublish = 'needs-publish'; - static const String _statusMessageNoPublish = 'no-publish'; - static const String _statusMessageError = 'error'; - static const String _statusKey = 'status'; - static const String _humanMessageKey = 'humanMessage'; - - @override - final String name = 'publish-check'; - - @override - final String description = - 'Checks to make sure that a package *could* be published.'; - - final PubVersionFinder _pubVersionFinder; - - /// The overall result of the run for machine-readable output. This is the - /// highest value that occurs during the run. - _PublishCheckResult _overallResult = _PublishCheckResult.nothingToPublish; - - @override - bool get captureOutput => getBoolArg(_machineFlag); - - @override - Future initializeRun() async { - _overallResult = _PublishCheckResult.nothingToPublish; - } - - @override - Future runForPackage(RepositoryPackage package) async { - _PublishCheckResult? result = await _passesPublishCheck(package); - if (result == null) { - return PackageResult.skip('Package is marked as unpublishable.'); - } - if (!_passesAuthorsCheck(package)) { - _printImportantStatusMessage( - 'No AUTHORS file found. Packages must include an AUTHORS file.', - isError: true); - result = _PublishCheckResult.error; - } - - if (result.index > _overallResult.index) { - _overallResult = result; - } - return result == _PublishCheckResult.error - ? PackageResult.fail() - : PackageResult.success(); - } - - @override - Future completeRun() async { - _pubVersionFinder.httpClient.close(); - } - - @override - Future handleCapturedOutput(List output) async { - final Map machineOutput = { - _statusKey: _statusStringForResult(_overallResult), - _humanMessageKey: output, - }; - - print(const JsonEncoder.withIndent(' ').convert(machineOutput)); - } - - String _statusStringForResult(_PublishCheckResult result) { - switch (result) { - case _PublishCheckResult.nothingToPublish: - return _statusMessageNoPublish; - case _PublishCheckResult.needsPublishing: - return _statusNeedsPublish; - case _PublishCheckResult.error: - return _statusMessageError; - } - } - - Pubspec? _tryParsePubspec(RepositoryPackage package) { - try { - return package.parsePubspec(); - } on Exception catch (exception) { - print( - 'Failed to parse `pubspec.yaml` at ${package.pubspecFile.path}: ' - '$exception', - ); - return null; - } - } - - // Run `dart pub get` on the examples of [package]. - Future _fetchExampleDeps(RepositoryPackage package) async { - for (final RepositoryPackage example in package.getExamples()) { - await processRunner.runAndStream( - 'dart', - ['pub', 'get'], - workingDir: example.directory, - ); - } - } - - Future _hasValidPublishCheckRun(RepositoryPackage package) async { - // `pub publish` does not do `dart pub get` inside `example` directories - // of a package (but they're part of the analysis output!). - // Issue: https://github.com/flutter/flutter/issues/113788 - await _fetchExampleDeps(package); - - print('Running pub publish --dry-run:'); - final io.Process process = await processRunner.start( - flutterCommand, - ['pub', 'publish', '--', '--dry-run'], - workingDirectory: package.directory, - ); - - final StringBuffer outputBuffer = StringBuffer(); - - final Completer stdOutCompleter = Completer(); - process.stdout.listen( - (List event) { - final String output = String.fromCharCodes(event); - if (output.isNotEmpty) { - print(output); - outputBuffer.write(output); - } - }, - onDone: () => stdOutCompleter.complete(), - ); - - final Completer stdInCompleter = Completer(); - process.stderr.listen( - (List event) { - final String output = String.fromCharCodes(event); - if (output.isNotEmpty) { - // The final result is always printed on stderr, whether success or - // failure. - final bool isError = !output.contains('has 0 warnings'); - _printImportantStatusMessage(output, isError: isError); - outputBuffer.write(output); - } - }, - onDone: () => stdInCompleter.complete(), - ); - - if (await process.exitCode == 0) { - return true; - } - - if (!getBoolArg(_allowPrereleaseFlag)) { - return false; - } - - 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.'); - } - - /// Returns the result of the publish check, or null if the package is marked - /// as unpublishable. - Future<_PublishCheckResult?> _passesPublishCheck( - RepositoryPackage package) async { - final String packageName = package.directory.basename; - final Pubspec? pubspec = _tryParsePubspec(package); - if (pubspec == null) { - print('No valid pubspec found.'); - return _PublishCheckResult.error; - } else if (pubspec.publishTo == 'none') { - return null; - } - - final Version? version = pubspec.version; - final _PublishCheckResult alreadyPublishedResult = - await _checkPublishingStatus( - packageName: packageName, version: version); - if (alreadyPublishedResult == _PublishCheckResult.nothingToPublish) { - print( - 'Package $packageName version: $version has already be published on pub.'); - return alreadyPublishedResult; - } else if (alreadyPublishedResult == _PublishCheckResult.error) { - print('Check pub version failed $packageName'); - return _PublishCheckResult.error; - } - - if (await _hasValidPublishCheckRun(package)) { - print('Package $packageName is able to be published.'); - return _PublishCheckResult.needsPublishing; - } else { - print('Unable to publish $packageName'); - return _PublishCheckResult.error; - } - } - - // Check if `packageName` already has `version` published on pub. - Future<_PublishCheckResult> _checkPublishingStatus( - {required String packageName, required Version? version}) async { - final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(packageName: packageName); - switch (pubVersionFinderResponse.result) { - case PubVersionFinderResult.success: - return pubVersionFinderResponse.versions.contains(version) - ? _PublishCheckResult.nothingToPublish - : _PublishCheckResult.needsPublishing; - case PubVersionFinderResult.fail: - print(''' -Error fetching version on pub for $packageName. -HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} -HTTP response: ${pubVersionFinderResponse.httpResponse.body} -'''); - return _PublishCheckResult.error; - case PubVersionFinderResult.noPackageFound: - return _PublishCheckResult.needsPublishing; - } - } - - bool _passesAuthorsCheck(RepositoryPackage package) { - final List pathComponents = - package.directory.fileSystem.path.split(package.path); - if (pathComponents.contains('third_party')) { - // Third-party packages aren't required to have an AUTHORS file. - return true; - } - return package.authorsFile.existsSync(); - } - - void _printImportantStatusMessage(String message, {required bool isError}) { - final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message'; - if (getBoolArg(_machineFlag)) { - print(statusMessage); - } else { - if (isError) { - printError(statusMessage); - } else { - printSuccess(statusMessage); - } - } - } -} - -/// Possible outcomes of of a publishing check. -enum _PublishCheckResult { - nothingToPublish, - needsPublishing, - error, -} diff --git a/script/tool/lib/src/publish_command.dart b/script/tool/lib/src/publish_command.dart deleted file mode 100644 index e7b3d110c5fa..000000000000 --- a/script/tool/lib/src/publish_command.dart +++ /dev/null @@ -1,456 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:yaml/yaml.dart'; - -import 'common/core.dart'; -import 'common/file_utils.dart'; -import 'common/git_version_finder.dart'; -import 'common/package_command.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/pub_version_finder.dart'; -import 'common/repository_package.dart'; - -@immutable -class _RemoteInfo { - const _RemoteInfo({required this.name, required this.url}); - - /// The git name for the remote. - final String name; - - /// The remote's URL. - final String url; -} - -/// 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` for full -/// usage information. -/// -/// [processRunner], [print], and [stdin] can be overriden for easier testing. -class PublishCommand extends PackageLoopingCommand { - /// Creates an instance of the publish command. - PublishCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - io.Stdin? stdinput, - GitDir? gitDir, - http.Client? httpClient, - }) : _pubVersionFinder = - PubVersionFinder(httpClient: httpClient ?? http.Client()), - _stdin = stdinput ?? io.stdin, - super(packagesDir, - platform: platform, processRunner: processRunner, gitDir: gitDir) { - argParser.addMultiOption(_pubFlagsOption, - help: - 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); - argParser.addOption( - _remoteOption, - help: 'The name of the remote to push the tags to.', - // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. - defaultsTo: 'upstream', - ); - argParser.addFlag( - _allChangedFlag, - help: - 'Release all packages that contains pubspec changes at the current commit compares to the base-sha.\n' - 'The --packages option is ignored if this is on.', - ); - argParser.addFlag( - _dryRunFlag, - help: - 'Skips the real `pub publish` and `git tag` commands and assumes both commands are successful.\n' - 'This does not run `pub publish --dry-run`.\n' - 'If you want to run the command with `pub publish --dry-run`, use `pub-publish-flags=--dry-run`', - ); - argParser.addFlag(_skipConfirmationFlag, - help: 'Run the command without asking for Y/N inputs.\n' - 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n'); - } - - static const String _pubFlagsOption = 'pub-publish-flags'; - static const String _remoteOption = 'remote'; - static const String _allChangedFlag = 'all-changed'; - static const String _dryRunFlag = 'dry-run'; - static const String _skipConfirmationFlag = 'skip-confirmation'; - - static const String _pubCredentialName = 'PUB_CREDENTIALS'; - - // 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'; - - @override - final String description = - 'Attempts to publish the given packages and tag the release(s) on GitHub.\n' - 'If running this on CI, an environment variable named $_pubCredentialName must be set to a String that represents the pub credential JSON.\n' - 'WARNING: Do not check in the content of pub credential JSON, it should only come from secure sources.'; - - final io.Stdin _stdin; - StreamSubscription? _stdinSubscription; - final PubVersionFinder _pubVersionFinder; - - // Tags that already exist in the repository. - List _existingGitTags = []; - // The remote to push tags to. - late _RemoteInfo _remote; - // Flags to pass to `pub publish`. - late List _publishFlags; - - @override - String get successSummaryMessage => 'published'; - - @override - String get failureListHeader => - 'The following packages had failures during publishing:'; - - @override - Future initializeRun() async { - print('Checking local repo...'); - - // Ensure that the requested remote is present. - final String remoteName = getStringArg(_remoteOption); - final String? remoteUrl = await _verifyRemote(remoteName); - if (remoteUrl == null) { - printError('Unable to find URL for remote $remoteName; cannot push tags'); - throw ToolExit(1); - } - _remote = _RemoteInfo(name: remoteName, url: remoteUrl); - - // Pre-fetch all the repository's tags, to check against when publishing. - final GitDir repository = await gitDir; - final io.ProcessResult existingTagsResult = - await repository.runCommand(['tag', '--sort=-committerdate']); - _existingGitTags = (existingTagsResult.stdout as String).split('\n') - ..removeWhere((String element) => element.isEmpty); - - _publishFlags = [ - ...getStringListArg(_pubFlagsOption), - if (getBoolArg(_skipConfirmationFlag)) '--force', - ]; - - if (getBoolArg(_dryRunFlag)) { - print('=============== DRY RUN ==============='); - } - } - - @override - Stream getPackagesToProcess() async* { - if (getBoolArg(_allChangedFlag)) { - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - final String baseSha = await gitVersionFinder.getBaseSha(); - print( - 'Publishing all packages that have changed relative to "$baseSha"\n'); - final List changedPubspecs = - await gitVersionFinder.getChangedPubSpecs(); - - for (final String pubspecPath in changedPubspecs) { - // git outputs a relativa, Posix-style path. - final File pubspecFile = childFileWithSubcomponents( - packagesDir.fileSystem.directory((await gitDir).path), - p.posix.split(pubspecPath)); - yield PackageEnumerationEntry(RepositoryPackage(pubspecFile.parent), - excluded: false); - } - } else { - yield* getTargetPackages(filterExcluded: false); - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - final PackageResult? checkResult = await _checkNeedsRelease(package); - if (checkResult != null) { - return checkResult; - } - - if (!await _checkGitStatus(package)) { - return PackageResult.fail(['uncommitted changes']); - } - - if (!await _publish(package)) { - return PackageResult.fail(['publish failed']); - } - - if (!await _tagRelease(package)) { - return PackageResult.fail(['tagging failed']); - } - - print('\nPublished ${package.directory.basename} successfully!'); - return PackageResult.success(); - } - - @override - Future completeRun() async { - _pubVersionFinder.httpClient.close(); - await _stdinSubscription?.cancel(); - _stdinSubscription = null; - } - - /// Checks whether [package] needs to be released, printing check status and - /// returning one of: - /// - PackageResult.fail if the check could not be completed - /// - PackageResult.skip if no release is necessary - /// - null if releasing should proceed - /// - /// In cases where a non-null result is returned, that should be returned - /// as the final result for the package, without further processing. - Future _checkNeedsRelease(RepositoryPackage package) async { - if (!package.pubspecFile.existsSync()) { - logWarning(''' -The pubspec file for ${package.displayName} does not exist, so no publishing will happen. -Safe to ignore if the package is deleted in this commit. -'''); - return PackageResult.skip('package deleted'); - } - - final Pubspec pubspec = package.parsePubspec(); - - if (pubspec.name == 'flutter_plugin_tools') { - // Ignore flutter_plugin_tools package when running publishing through flutter_plugin_tools. - // TODO(cyanglaz): Make the tool also auto publish flutter_plugin_tools package. - // https://github.com/flutter/flutter/issues/85430 - return PackageResult.skip( - 'publishing flutter_plugin_tools via the tool is not supported'); - } - - if (pubspec.publishTo == 'none') { - return PackageResult.skip('publish_to: none'); - } - - if (pubspec.version == null) { - printError( - 'No version found. A package that intentionally has no version should be marked "publish_to: none"'); - return PackageResult.fail(['no version']); - } - - // Check if the package named `packageName` with `version` has already - // been published. - final Version version = pubspec.version!; - final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(packageName: pubspec.name); - if (pubVersionFinderResponse.versions.contains(version)) { - final String tagsForPackageWithSameVersion = _existingGitTags.firstWhere( - (String tag) => - tag.split('-v').first == pubspec.name && - tag.split('-v').last == version.toString(), - orElse: () => ''); - if (tagsForPackageWithSameVersion.isEmpty) { - printError( - '${pubspec.name} $version has already been published, however ' - 'the git release tag (${pubspec.name}-v$version) was not found. ' - 'Please manually fix the tag then run the command again.'); - return PackageResult.fail(['published but untagged']); - } else { - print('${pubspec.name} $version has already been published.'); - return PackageResult.skip('already published'); - } - } - return null; - } - - // Tag the release with -v, and push it to the remote. - // - // Return `true` if successful, `false` otherwise. - Future _tagRelease(RepositoryPackage package) async { - final String tag = _getTag(package); - print('Tagging release $tag...'); - if (!getBoolArg(_dryRunFlag)) { - final io.ProcessResult result = await (await gitDir).runCommand( - ['tag', tag], - throwOnError: false, - ); - if (result.exitCode != 0) { - return false; - } - } - - print('Pushing tag to ${_remote.name}...'); - final bool success = await _pushTagToRemote( - tag: tag, - remote: _remote, - ); - if (success) { - print('Release tagged!'); - } - return success; - } - - Future _checkGitStatus(RepositoryPackage package) async { - final io.ProcessResult statusResult = await (await gitDir).runCommand( - [ - 'status', - '--porcelain', - '--ignored', - package.directory.absolute.path - ], - throwOnError: false, - ); - if (statusResult.exitCode != 0) { - return false; - } - - final String statusOutput = statusResult.stdout as String; - if (statusOutput.isNotEmpty) { - printError( - "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.'); - } - return statusOutput.isEmpty; - } - - Future _verifyRemote(String remote) async { - final io.ProcessResult getRemoteUrlResult = await (await gitDir).runCommand( - ['remote', 'get-url', remote], - throwOnError: false, - ); - if (getRemoteUrlResult.exitCode != 0) { - return null; - } - return getRemoteUrlResult.stdout as String?; - } - - Future _publish(RepositoryPackage package) async { - print('Publishing...'); - print('Running `pub publish ${_publishFlags.join(' ')}` in ' - '${package.directory.absolute.path}...\n'); - if (getBoolArg(_dryRunFlag)) { - return true; - } - - if (_publishFlags.contains('--force')) { - _ensureValidPubCredential(); - } - - final io.Process publish = await processRunner.start( - flutterCommand, ['pub', 'publish', ..._publishFlags], - workingDirectory: package.directory); - 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) { - printError('Publishing ${package.directory.basename} failed.'); - return false; - } - - print('Package published!'); - return true; - } - - String _getTag(RepositoryPackage package) { - final File pubspecFile = package.pubspecFile; - final YamlMap pubspecYaml = - loadYaml(pubspecFile.readAsStringSync()) as YamlMap; - final String name = pubspecYaml['name'] as String; - final String version = pubspecYaml['version'] as String; - // We should have failed to publish if these were unset. - assert(name.isNotEmpty && version.isNotEmpty); - return _tagFormat - .replaceAll('%PACKAGE%', name) - .replaceAll('%VERSION%', version); - } - - // Pushes the `tag` to `remote` - // - // Return `true` if successful, `false` otherwise. - Future _pushTagToRemote({ - required String tag, - required _RemoteInfo remote, - }) async { - assert(remote != null && tag != null); - if (!getBoolArg(_dryRunFlag)) { - final io.ProcessResult result = await (await gitDir).runCommand( - ['push', remote.name, tag], - throwOnError: false, - ); - if (result.exitCode != 0) { - return false; - } - } - return true; - } - - void _ensureValidPubCredential() { - final String credentialsPath = _credentialsPath; - final File credentialFile = packagesDir.fileSystem.file(credentialsPath); - if (credentialFile.existsSync() && - credentialFile.readAsStringSync().isNotEmpty) { - return; - } - final String? credential = io.Platform.environment[_pubCredentialName]; - if (credential == null) { - printError(''' -No pub credential available. Please check if `$credentialsPath` is valid. -If running this command on CI, you can set the pub credential content in the $_pubCredentialName environment variable. -'''); - throw ToolExit(1); - } - credentialFile.openSync(mode: FileMode.writeOnlyAppend) - ..writeStringSync(credential) - ..closeSync(); - } - - /// Returns the correct path where the pub credential is stored. - @visibleForTesting - static String getCredentialPath() { - return _credentialsPath; - } -} - -/// The path in which pub expects to find its credentials file. -final String _credentialsPath = () { - // This follows the same logic as pub: - // https://github.com/dart-lang/pub/blob/d99b0d58f4059d7bb4ac4616fd3d54ec00a2b5d4/lib/src/system_cache.dart#L34-L43 - String? cacheDir; - final String? pubCache = io.Platform.environment['PUB_CACHE']; - if (pubCache != null) { - cacheDir = pubCache; - } else if (io.Platform.isWindows) { - final String? appData = io.Platform.environment['APPDATA']; - if (appData == null) { - printError('"APPDATA" environment variable is not set.'); - } else { - cacheDir = p.join(appData, 'Pub', 'Cache'); - } - } else { - final String? home = io.Platform.environment['HOME']; - if (home == null) { - printError('"HOME" environment variable is not set.'); - } else { - cacheDir = p.join(home, '.pub-cache'); - } - } - - if (cacheDir == null) { - printError('Unable to determine pub cache location'); - throw ToolExit(1); - } - - return p.join(cacheDir, 'credentials.json'); -}(); diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart deleted file mode 100644 index 5682ba057688..000000000000 --- a/script/tool/lib/src/pubspec_check_command.dart +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:platform/platform.dart'; -import 'package:yaml/yaml.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// A command to enforce pubspec conventions across the repository. -/// -/// This both ensures that repo best practices for which optional fields are -/// used are followed, and that the structure is consistent to make edits -/// across multiple pubspec files easier. -class PubspecCheckCommand extends PackageLoopingCommand { - /// Creates an instance of the version check command. - PubspecCheckCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - GitDir? gitDir, - }) : super( - packagesDir, - processRunner: processRunner, - platform: platform, - gitDir: gitDir, - ); - - // Section order for plugins. Because the 'flutter' section is critical - // information for plugins, and usually small, it goes near the top unlike in - // a normal app or package. - static const List _majorPluginSections = [ - 'environment:', - 'flutter:', - 'dependencies:', - 'dev_dependencies:', - 'false_secrets:', - ]; - - static const List _majorPackageSections = [ - 'environment:', - 'dependencies:', - 'dev_dependencies:', - 'flutter:', - 'false_secrets:', - ]; - - static const String _expectedIssueLinkFormat = - 'https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A'; - - @override - final String name = 'pubspec-check'; - - @override - final String description = - 'Checks that pubspecs follow repository conventions.'; - - @override - bool get hasLongOutput => false; - - @override - PackageLoopingType get packageLoopingType => - PackageLoopingType.includeAllSubpackages; - - @override - Future runForPackage(RepositoryPackage package) async { - final File pubspec = package.pubspecFile; - final bool passesCheck = - !pubspec.existsSync() || await _checkPubspec(pubspec, package: package); - if (!passesCheck) { - return PackageResult.fail(); - } - return PackageResult.success(); - } - - Future _checkPubspec( - File pubspecFile, { - required RepositoryPackage package, - }) async { - final String contents = pubspecFile.readAsStringSync(); - final Pubspec? pubspec = _tryParsePubspec(contents); - if (pubspec == null) { - return false; - } - - final List pubspecLines = contents.split('\n'); - final bool isPlugin = pubspec.flutter?.containsKey('plugin') ?? false; - final List sectionOrder = - isPlugin ? _majorPluginSections : _majorPackageSections; - bool passing = _checkSectionOrder(pubspecLines, sectionOrder); - if (!passing) { - printError('${indentation}Major sections should follow standard ' - 'repository ordering:'); - final String listIndentation = indentation * 2; - printError('$listIndentation${sectionOrder.join('\n$listIndentation')}'); - } - - if (isPlugin) { - final String? implementsError = - _checkForImplementsError(pubspec, package: package); - if (implementsError != null) { - printError('$indentation$implementsError'); - passing = false; - } - - final String? defaultPackageError = - _checkForDefaultPackageError(pubspec, package: package); - if (defaultPackageError != null) { - printError('$indentation$defaultPackageError'); - passing = false; - } - } - - // Ignore metadata that's only relevant for published packages if the - // packages is not intended for publishing. - if (pubspec.publishTo != 'none') { - final List repositoryErrors = - _checkForRepositoryLinkErrors(pubspec, package: package); - if (repositoryErrors.isNotEmpty) { - for (final String error in repositoryErrors) { - printError('$indentation$error'); - } - passing = false; - } - - if (!_checkIssueLink(pubspec)) { - printError( - '${indentation}A package should have an "issue_tracker" link to a ' - 'search for open flutter/flutter bugs with the relevant label:\n' - '${indentation * 2}$_expectedIssueLinkFormat'); - passing = false; - } - - // Don't check descriptions for federated package components other than - // the app-facing package, since they are unlisted, and are expected to - // have short descriptions. - if (!package.isPlatformInterface && !package.isPlatformImplementation) { - final String? descriptionError = - _checkDescription(pubspec, package: package); - if (descriptionError != null) { - printError('$indentation$descriptionError'); - passing = false; - } - } - } - - return passing; - } - - Pubspec? _tryParsePubspec(String pubspecContents) { - try { - return Pubspec.parse(pubspecContents); - } on Exception catch (exception) { - print(' Cannot parse pubspec.yaml: $exception'); - } - return null; - } - - bool _checkSectionOrder( - List pubspecLines, List sectionOrder) { - int previousSectionIndex = 0; - for (final String line in pubspecLines) { - final int index = sectionOrder.indexOf(line); - if (index == -1) { - continue; - } - if (index < previousSectionIndex) { - return false; - } - previousSectionIndex = index; - } - return true; - } - - List _checkForRepositoryLinkErrors( - Pubspec pubspec, { - required RepositoryPackage package, - }) { - final List errorMessages = []; - if (pubspec.repository == null) { - errorMessages.add('Missing "repository"'); - } else { - final String relativePackagePath = - getRelativePosixPath(package.directory, from: packagesDir.parent); - if (!pubspec.repository!.path.endsWith(relativePackagePath)) { - errorMessages - .add('The "repository" link should end with the package path.'); - } - - if (pubspec.repository!.path.contains('/master/')) { - errorMessages - .add('The "repository" link should use "main", not "master".'); - } - } - - if (pubspec.homepage != null) { - errorMessages - .add('Found a "homepage" entry; only "repository" should be used.'); - } - - return errorMessages; - } - - // Validates the "description" field for a package, returning an error - // string if there are any issues. - String? _checkDescription( - Pubspec pubspec, { - required RepositoryPackage package, - }) { - final String? description = pubspec.description; - if (description == null) { - return 'Missing "description"'; - } - - if (description.length < 60) { - return '"description" is too short. pub.dev recommends package ' - 'descriptions of 60-180 characters.'; - } - if (description.length > 180) { - return '"description" is too long. pub.dev recommends package ' - 'descriptions of 60-180 characters.'; - } - return null; - } - - bool _checkIssueLink(Pubspec pubspec) { - return pubspec.issueTracker - ?.toString() - .startsWith(_expectedIssueLinkFormat) ?? - false; - } - - // Validates the "implements" keyword for a plugin, returning an error - // string if there are any issues. - // - // Should only be called on plugin packages. - String? _checkForImplementsError( - Pubspec pubspec, { - required RepositoryPackage package, - }) { - if (_isImplementationPackage(package)) { - final YamlMap pluginSection = pubspec.flutter!['plugin'] as YamlMap; - final String? implements = pluginSection['implements'] as String?; - final String expectedImplements = package.directory.parent.basename; - if (implements == null) { - return 'Missing "implements: $expectedImplements" in "plugin" section.'; - } else if (implements != expectedImplements) { - return 'Expecetd "implements: $expectedImplements"; ' - 'found "implements: $implements".'; - } - } - return null; - } - - // Validates any "default_package" entries a plugin, returning an error - // string if there are any issues. - // - // Should only be called on plugin packages. - String? _checkForDefaultPackageError( - Pubspec pubspec, { - required RepositoryPackage package, - }) { - final YamlMap pluginSection = pubspec.flutter!['plugin'] as YamlMap; - final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; - if (platforms == null) { - logWarning('Does not implement any platforms'); - return null; - } - final String packageName = package.directory.basename; - - // Validate that the default_package entries look correct (e.g., no typos). - final Set defaultPackages = {}; - for (final MapEntry platformEntry in platforms.entries) { - final YamlMap platformDetails = platformEntry.value! as YamlMap; - final String? defaultPackage = - platformDetails['default_package'] as String?; - if (defaultPackage != null) { - defaultPackages.add(defaultPackage); - if (!defaultPackage.startsWith('${packageName}_')) { - return '"$defaultPackage" is not an expected implementation name ' - 'for "$packageName"'; - } - } - } - - // Validate that all default_packages are also dependencies. - final Iterable dependencies = pubspec.dependencies.keys; - final Iterable missingPackages = defaultPackages - .where((String package) => !dependencies.contains(package)); - if (missingPackages.isNotEmpty) { - return 'The following default_packages are missing ' - 'corresponding dependencies:\n' - ' ${missingPackages.join('\n ')}'; - } - - return null; - } - - // Returns true if [packageName] appears to be an implementation package - // according to repository conventions. - bool _isImplementationPackage(RepositoryPackage package) { - if (!package.isFederated) { - return false; - } - final String packageName = package.directory.basename; - final String parentName = package.directory.parent.basename; - // A few known package names are not implementation packages; assume - // anything else is. (This is done instead of listing known implementation - // suffixes to allow for non-standard suffixes; e.g., to put several - // platforms in one package for code-sharing purposes.) - const Set nonImplementationSuffixes = { - '', // App-facing package. - '_platform_interface', // Platform interface package. - }; - final String suffix = packageName.substring(parentName.length); - return !nonImplementationSuffixes.contains(suffix); - } -} diff --git a/script/tool/lib/src/readme_check_command.dart b/script/tool/lib/src/readme_check_command.dart deleted file mode 100644 index cbbb8b835a13..000000000000 --- a/script/tool/lib/src/readme_check_command.dart +++ /dev/null @@ -1,344 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:platform/platform.dart'; -import 'package:yaml/yaml.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -const String _instructionWikiUrl = - 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages'; - -/// A command to enforce README conventions across the repository. -class ReadmeCheckCommand extends PackageLoopingCommand { - /// Creates an instance of the README check command. - ReadmeCheckCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - GitDir? gitDir, - }) : super( - packagesDir, - processRunner: processRunner, - platform: platform, - gitDir: gitDir, - ) { - argParser.addFlag(_requireExcerptsArg, - help: 'Require that Dart code blocks be managed by code-excerpt.'); - } - - static const String _requireExcerptsArg = 'require-excerpts'; - - // Standardized capitalizations for platforms that a plugin can support. - static const Map _standardPlatformNames = { - 'android': 'Android', - 'ios': 'iOS', - 'linux': 'Linux', - 'macos': 'macOS', - 'web': 'Web', - 'windows': 'Windows', - }; - - @override - final String name = 'readme-check'; - - @override - final String description = - 'Checks that READMEs follow repository conventions.'; - - @override - bool get hasLongOutput => false; - - @override - Future runForPackage(RepositoryPackage package) async { - final List errors = _validateReadme(package.readmeFile, - mainPackage: package, isExample: false); - for (final RepositoryPackage packageToCheck in package.getExamples()) { - errors.addAll(_validateReadme(packageToCheck.readmeFile, - mainPackage: package, isExample: true)); - } - - // If there's an example/README.md for a multi-example package, validate - // that as well, as it will be shown on pub.dev. - final Directory exampleDir = package.directory.childDirectory('example'); - final File exampleDirReadme = exampleDir.childFile('README.md'); - if (exampleDir.existsSync() && !isPackage(exampleDir)) { - errors.addAll(_validateReadme(exampleDirReadme, - mainPackage: package, isExample: true)); - } - - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - List _validateReadme(File readme, - {required RepositoryPackage mainPackage, required bool isExample}) { - if (!readme.existsSync()) { - if (isExample) { - print('${indentation}No README for ' - '${getRelativePosixPath(readme.parent, from: mainPackage.directory)}'); - return []; - } else { - printError('${indentation}No README found at ' - '${getRelativePosixPath(readme, from: mainPackage.directory)}'); - return ['Missing README.md']; - } - } - - print('${indentation}Checking ' - '${getRelativePosixPath(readme, from: mainPackage.directory)}...'); - - final List readmeLines = readme.readAsLinesSync(); - final List errors = []; - - final String? blockValidationError = - _validateCodeBlocks(readmeLines, mainPackage: mainPackage); - if (blockValidationError != null) { - errors.add(blockValidationError); - } - - errors.addAll(_validateBoilerplate(readmeLines, - mainPackage: mainPackage, isExample: isExample)); - - // Check if this is the main readme for a plugin, and if so enforce extra - // checks. - if (!isExample) { - final Pubspec pubspec = mainPackage.parsePubspec(); - final bool isPlugin = pubspec.flutter?['plugin'] != null; - if (isPlugin && (!mainPackage.isFederated || mainPackage.isAppFacing)) { - final String? error = _validateSupportedPlatforms(readmeLines, pubspec); - if (error != null) { - errors.add(error); - } - } - } - - return errors; - } - - /// Validates that code blocks (``` ... ```) follow repository standards. - String? _validateCodeBlocks( - List readmeLines, { - required RepositoryPackage mainPackage, - }) { - final RegExp codeBlockDelimiterPattern = RegExp(r'^\s*```\s*([^ ]*)\s*'); - const String excerptTagStart = ' missingLanguageLines = []; - final List missingExcerptLines = []; - bool inBlock = false; - for (int i = 0; i < readmeLines.length; ++i) { - final RegExpMatch? match = - codeBlockDelimiterPattern.firstMatch(readmeLines[i]); - if (match == null) { - continue; - } - if (inBlock) { - inBlock = false; - continue; - } - inBlock = true; - - final int humanReadableLineNumber = i + 1; - - // Ensure that there's a language tag. - final String infoString = match[1] ?? ''; - if (infoString.isEmpty) { - missingLanguageLines.add(humanReadableLineNumber); - continue; - } - - // Check for code-excerpt usage if requested. - if (getBoolArg(_requireExcerptsArg) && infoString == 'dart') { - if (i == 0 || !readmeLines[i - 1].trim().startsWith(excerptTagStart)) { - missingExcerptLines.add(humanReadableLineNumber); - } - } - } - - String? errorSummary; - - if (missingLanguageLines.isNotEmpty) { - for (final int lineNumber in missingLanguageLines) { - printError('${indentation}Code block at line $lineNumber is missing ' - 'a language identifier.'); - } - printError( - '\n${indentation}For each block listed above, add a language tag to ' - 'the opening block. For instance, for Dart code, use:\n' - '${indentation * 2}```dart\n'); - errorSummary = 'Missing language identifier for code block'; - } - - // If any blocks use code excerpts, make sure excerpting is configured - // for the package. - if (readmeLines.any((String line) => line.startsWith(excerptTagStart))) { - const String buildRunnerConfigFile = 'build.excerpt.yaml'; - if (!mainPackage.getExamples().any((RepositoryPackage example) => - example.directory.childFile(buildRunnerConfigFile).existsSync())) { - printError('code-excerpt tag found, but the package is not configured ' - 'for excerpting. Follow the instructions at\n' - '$_instructionWikiUrl\n' - 'for setting up a build.excerpt.yaml file.'); - errorSummary ??= 'Missing code-excerpt configuration'; - } - } - - if (missingExcerptLines.isNotEmpty) { - for (final int lineNumber in missingExcerptLines) { - printError('${indentation}Dart code block at line $lineNumber is not ' - 'managed by code-excerpt.'); - } - printError( - '\n${indentation}For each block listed above, add ' - 'tag on the previous line, and ensure that a build.excerpt.yaml is ' - 'configured for the source example as explained at\n' - '$_instructionWikiUrl'); - errorSummary ??= 'Missing code-excerpt management for code block'; - } - - return errorSummary; - } - - /// Validates that the plugin has a supported platforms table following the - /// expected format, returning an error string if any issues are found. - String? _validateSupportedPlatforms( - List readmeLines, Pubspec pubspec) { - // Example table following expected format: - // | | Android | iOS | Web | - // |----------------|---------|----------|------------------------| - // | **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | - final int detailsLineNumber = readmeLines - .indexWhere((String line) => line.startsWith('| **Support**')); - if (detailsLineNumber == -1) { - return 'No OS support table found'; - } - final int osLineNumber = detailsLineNumber - 2; - if (osLineNumber < 0 || !readmeLines[osLineNumber].startsWith('|')) { - return 'OS support table does not have the expected header format'; - } - - // Utility method to convert an iterable of strings to a case-insensitive - // sorted, comma-separated string of its elements. - String sortedListString(Iterable entries) { - final List entryList = entries.toList(); - entryList.sort( - (String a, String b) => a.toLowerCase().compareTo(b.toLowerCase())); - return entryList.join(', '); - } - - // Validate that the supported OS lists match. - final YamlMap pluginSection = pubspec.flutter!['plugin'] as YamlMap; - final dynamic platformsEntry = pluginSection['platforms']; - if (platformsEntry == null) { - logWarning('Plugin not support any platforms'); - return null; - } - final YamlMap platformSupportMaps = platformsEntry as YamlMap; - final Set actuallySupportedPlatform = - platformSupportMaps.keys.toSet().cast(); - final Iterable documentedPlatforms = readmeLines[osLineNumber] - .split('|') - .map((String entry) => entry.trim()) - .where((String entry) => entry.isNotEmpty); - final Set documentedPlatformsLowercase = - documentedPlatforms.map((String entry) => entry.toLowerCase()).toSet(); - if (actuallySupportedPlatform.length != documentedPlatforms.length || - actuallySupportedPlatform - .intersection(documentedPlatformsLowercase) - .length != - actuallySupportedPlatform.length) { - printError(''' -${indentation}OS support table does not match supported platforms: -${indentation * 2}Actual: ${sortedListString(actuallySupportedPlatform)} -${indentation * 2}Documented: ${sortedListString(documentedPlatformsLowercase)} -'''); - return 'Incorrect OS support table'; - } - - // Enforce a standard set of capitalizations for the OS headings. - final Iterable incorrectCapitalizations = documentedPlatforms - .toSet() - .difference(_standardPlatformNames.values.toSet()); - if (incorrectCapitalizations.isNotEmpty) { - final Iterable expectedVersions = incorrectCapitalizations - .map((String name) => _standardPlatformNames[name.toLowerCase()]!); - printError(''' -${indentation}Incorrect OS capitalization: ${sortedListString(incorrectCapitalizations)} -${indentation * 2}Please use standard capitalizations: ${sortedListString(expectedVersions)} -'''); - return 'Incorrect OS support formatting'; - } - - // TODO(stuartmorgan): Add validation that the minimums in the table are - // consistent with what the current implementations require. See - // https://github.com/flutter/flutter/issues/84200 - return null; - } - - /// Validates [readmeLines], outputing error messages for any issue and - /// returning an array of error summaries (if any). - /// - /// Returns an empty array if validation passes. - List _validateBoilerplate( - List readmeLines, { - required RepositoryPackage mainPackage, - required bool isExample, - }) { - final List errors = []; - - if (_containsTemplateFlutterBoilerplate(readmeLines)) { - printError('${indentation}The boilerplate section about getting started ' - 'with Flutter should not be left in.'); - errors.add('Contains template boilerplate'); - } - - // Enforce a repository-standard message in implementation plugin examples, - // since they aren't typical examples, which has been a source of - // confusion for plugin clients who find them. - if (isExample && mainPackage.isPlatformImplementation) { - if (_containsExampleBoilerplate(readmeLines)) { - printError('${indentation}The boilerplate should not be left in for a ' - "federated plugin implementation package's example."); - errors.add('Contains template boilerplate'); - } - if (!_containsImplementationExampleExplanation(readmeLines)) { - printError('${indentation}The example README for a platform ' - 'implementation package should warn readers about its intended ' - 'use. Please copy the example README from another implementation ' - 'package in this repository.'); - errors.add('Missing implementation package example warning'); - } - } - - return errors; - } - - /// Returns true if the README still has unwanted parts of the boilerplate - /// from the `flutter create` templates. - bool _containsTemplateFlutterBoilerplate(List readmeLines) { - return readmeLines.any((String line) => - line.contains('For help getting started with Flutter')); - } - - /// Returns true if the README still has the generic description of an - /// example from the `flutter create` templates. - bool _containsExampleBoilerplate(List readmeLines) { - return readmeLines - .any((String line) => line.contains('Demonstrates how to use the')); - } - - /// Returns true if the README contains the repository-standard explanation of - /// the purpose of a federated plugin implementation's example. - bool _containsImplementationExampleExplanation(List readmeLines) { - return readmeLines.contains('# Platform Implementation Test App') && - readmeLines - .any((String line) => line.contains('This is a test app for')); - } -} diff --git a/script/tool/lib/src/remove_dev_dependencies.dart b/script/tool/lib/src/remove_dev_dependencies.dart deleted file mode 100644 index 3085e0df85e0..000000000000 --- a/script/tool/lib/src/remove_dev_dependencies.dart +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:yaml/yaml.dart'; -import 'package:yaml_edit/yaml_edit.dart'; - -import 'common/package_looping_command.dart'; -import 'common/repository_package.dart'; - -/// A command to remove dev_dependencies, which are not used by package clients. -/// -/// This is intended for use with legacy Flutter version testing, to allow -/// running analysis (with --lib-only) with versions that are supported for -/// clients of the library, but not for development of the library. -class RemoveDevDependenciesCommand extends PackageLoopingCommand { - /// Creates a publish metadata updater command instance. - RemoveDevDependenciesCommand(Directory packagesDir) : super(packagesDir); - - @override - final String name = 'remove-dev-dependencies'; - - @override - final String description = 'Removes any dev_dependencies section from a ' - 'package, to allow more legacy testing.'; - - @override - bool get hasLongOutput => false; - - @override - PackageLoopingType get packageLoopingType => - PackageLoopingType.includeAllSubpackages; - - @override - Future runForPackage(RepositoryPackage package) async { - bool changed = false; - final YamlEditor editablePubspec = - YamlEditor(package.pubspecFile.readAsStringSync()); - const String devDependenciesKey = 'dev_dependencies'; - final YamlNode root = editablePubspec.parseAt([]); - final YamlMap? devDependencies = - (root as YamlMap)[devDependenciesKey] as YamlMap?; - if (devDependencies != null) { - changed = true; - print('${indentation}Removed dev_dependencies'); - editablePubspec.remove([devDependenciesKey]); - } - - if (changed) { - package.pubspecFile.writeAsStringSync(editablePubspec.toString()); - } - - return changed - ? PackageResult.success() - : PackageResult.skip('Nothing to remove.'); - } -} diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart deleted file mode 100644 index 5101b8f19e7e..000000000000 --- a/script/tool/lib/src/test_command.dart +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// A command to run Dart unit tests for packages. -class TestCommand extends PackageLoopingCommand { - /// Creates an instance of the test command. - TestCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addOption( - kEnableExperiment, - defaultsTo: '', - help: - 'Runs Dart unit tests in Dart VM with the given experiments enabled. ' - 'See https://github.com/dart-lang/sdk/blob/main/docs/process/experimental-flags.md ' - 'for details.', - ); - } - - @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 - PackageLoopingType get packageLoopingType => - PackageLoopingType.includeAllSubpackages; - - @override - Future runForPackage(RepositoryPackage package) async { - if (!package.testDirectory.existsSync()) { - return PackageResult.skip('No test/ directory.'); - } - - bool passed; - if (package.requiresFlutter()) { - passed = await _runFlutterTests(package); - } else { - passed = await _runDartTests(package); - } - return passed ? PackageResult.success() : PackageResult.fail(); - } - - /// Runs the Dart tests for a Flutter package, returning true on success. - Future _runFlutterTests(RepositoryPackage package) async { - final String experiment = getStringArg(kEnableExperiment); - - final int exitCode = await processRunner.runAndStream( - flutterCommand, - [ - 'test', - '--color', - if (experiment.isNotEmpty) '--enable-experiment=$experiment', - // TODO(ditman): Remove this once all plugins are migrated to 'drive'. - if (pluginSupportsPlatform(platformWeb, package)) '--platform=chrome', - ], - workingDir: package.directory, - ); - return exitCode == 0; - } - - /// Runs the Dart tests for a non-Flutter package, returning true on success. - Future _runDartTests(RepositoryPackage package) async { - // Unlike `flutter test`, `pub run test` does not automatically get - // packages - int exitCode = await processRunner.runAndStream( - 'dart', - ['pub', 'get'], - workingDir: package.directory, - ); - if (exitCode != 0) { - printError('Unable to fetch dependencies.'); - return false; - } - - final String experiment = getStringArg(kEnableExperiment); - - exitCode = await processRunner.runAndStream( - 'dart', - [ - 'run', - if (experiment.isNotEmpty) '--enable-experiment=$experiment', - 'test', - ], - workingDir: package.directory, - ); - - return exitCode == 0; - } -} diff --git a/script/tool/lib/src/update_excerpts_command.dart b/script/tool/lib/src/update_excerpts_command.dart deleted file mode 100644 index e65bed846cbc..000000000000 --- a/script/tool/lib/src/update_excerpts_command.dart +++ /dev/null @@ -1,233 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:platform/platform.dart'; -import 'package:yaml/yaml.dart'; -import 'package:yaml_edit/yaml_edit.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; - -/// A command to update README code excerpts from code files. -class UpdateExcerptsCommand extends PackageLoopingCommand { - /// Creates a excerpt updater command instance. - UpdateExcerptsCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - GitDir? gitDir, - }) : super( - packagesDir, - processRunner: processRunner, - platform: platform, - gitDir: gitDir, - ) { - argParser.addFlag(_failOnChangeFlag, hide: true); - } - - static const String _failOnChangeFlag = 'fail-on-change'; - - static const String _buildRunnerConfigName = 'excerpt'; - // The name of the build_runner configuration file that will be in an example - // directory if the package is set up to use `code-excerpt`. - static const String _buildRunnerConfigFile = - 'build.$_buildRunnerConfigName.yaml'; - - // The relative directory path to put the extracted excerpt yaml files. - static const String _excerptOutputDir = 'excerpts'; - - // The filename to store the pre-modification copy of the pubspec. - static const String _originalPubspecFilename = - 'pubspec.plugin_tools_original.yaml'; - - @override - final String name = 'update-excerpts'; - - @override - final String description = 'Updates code excerpts in README.md files, based ' - 'on code from code files, via code-excerpt'; - - @override - Future runForPackage(RepositoryPackage package) async { - final Iterable configuredExamples = package - .getExamples() - .where((RepositoryPackage example) => - example.directory.childFile(_buildRunnerConfigFile).existsSync()); - - if (configuredExamples.isEmpty) { - return PackageResult.skip( - 'No $_buildRunnerConfigFile found in example(s).'); - } - - final Directory repoRoot = - packagesDir.fileSystem.directory((await gitDir).path); - - for (final RepositoryPackage example in configuredExamples) { - _addSubmoduleDependencies(example, repoRoot: repoRoot); - - try { - // Ensure that dependencies are available. - final int pubGetExitCode = await processRunner.runAndStream( - 'dart', ['pub', 'get'], - workingDir: example.directory); - if (pubGetExitCode != 0) { - return PackageResult.fail( - ['Unable to get script dependencies']); - } - - // Update the excerpts. - if (!await _extractSnippets(example)) { - return PackageResult.fail(['Unable to extract excerpts']); - } - if (!await _injectSnippets(example, targetPackage: package)) { - return PackageResult.fail(['Unable to inject excerpts']); - } - } finally { - // Clean up the pubspec changes and extracted excerpts directory. - _undoPubspecChanges(example); - final Directory excerptDirectory = - example.directory.childDirectory(_excerptOutputDir); - if (excerptDirectory.existsSync()) { - excerptDirectory.deleteSync(recursive: true); - } - } - } - - if (getBoolArg(_failOnChangeFlag)) { - final String? stateError = await _validateRepositoryState(); - if (stateError != null) { - printError('README.md is out of sync with its source excerpts.\n\n' - 'If you edited code in README.md directly, you should instead edit ' - 'the example source files. If you edited source files, run the ' - 'repository tooling\'s "$name" command on this package, and update ' - 'your PR with the resulting changes.'); - return PackageResult.fail([stateError]); - } - } - - return PackageResult.success(); - } - - /// Runs the extraction step to create the excerpt files for the given - /// example, returning true on success. - Future _extractSnippets(RepositoryPackage example) async { - final int exitCode = await processRunner.runAndStream( - 'dart', - [ - 'run', - 'build_runner', - 'build', - '--config', - _buildRunnerConfigName, - '--output', - _excerptOutputDir, - '--delete-conflicting-outputs', - ], - workingDir: example.directory); - return exitCode == 0; - } - - /// Runs the injection step to update [targetPackage]'s README with the latest - /// excerpts from [example], returning true on success. - Future _injectSnippets( - RepositoryPackage example, { - required RepositoryPackage targetPackage, - }) async { - final String relativeReadmePath = - getRelativePosixPath(targetPackage.readmeFile, from: example.directory); - final int exitCode = await processRunner.runAndStream( - 'dart', - [ - 'run', - 'code_excerpt_updater', - '--write-in-place', - '--yaml', - '--no-escape-ng-interpolation', - relativeReadmePath, - ], - workingDir: example.directory); - return exitCode == 0; - } - - /// Adds `code_excerpter` and `code_excerpt_updater` to [package]'s - /// `dev_dependencies` using path-based references to the submodule copies. - /// - /// This is done on the fly rather than being checked in so that: - /// - Just building examples don't require everyone to check out submodules. - /// - Examples can be analyzed/built even on versions of Flutter that these - /// submodules do not support. - void _addSubmoduleDependencies(RepositoryPackage package, - {required Directory repoRoot}) { - final String pubspecContents = package.pubspecFile.readAsStringSync(); - // Save aside a copy of the current pubspec state. This allows restoration - // to the previous state regardless of its git status at the time the script - // ran. - package.directory - .childFile(_originalPubspecFilename) - .writeAsStringSync(pubspecContents); - - // Update the actual pubspec. - final YamlEditor editablePubspec = YamlEditor(pubspecContents); - const String devDependenciesKey = 'dev_dependencies'; - final YamlNode root = editablePubspec.parseAt([]); - // Ensure that there's a `dev_dependencies` entry to update. - if ((root as YamlMap)[devDependenciesKey] == null) { - editablePubspec.update(['dev_dependencies'], YamlMap()); - } - final Set submoduleDependencies = { - 'code_excerpter', - 'code_excerpt_updater', - }; - final String relativeRootPath = - getRelativePosixPath(repoRoot, from: package.directory); - for (final String dependency in submoduleDependencies) { - editablePubspec.update([ - devDependenciesKey, - dependency - ], { - 'path': '$relativeRootPath/site-shared/packages/$dependency' - }); - } - package.pubspecFile.writeAsStringSync(editablePubspec.toString()); - } - - /// Restores the version of the pubspec that was present before running - /// [_addSubmoduleDependencies]. - void _undoPubspecChanges(RepositoryPackage package) { - package.directory - .childFile(_originalPubspecFilename) - .renameSync(package.pubspecFile.path); - } - - /// Checks the git state, returning an error string if any .md files have - /// changed. - Future _validateRepositoryState() async { - final io.ProcessResult checkFiles = await processRunner.run( - 'git', - ['ls-files', '--modified'], - workingDir: packagesDir, - logOnError: true, - ); - if (checkFiles.exitCode != 0) { - return 'Unable to determine local file state'; - } - - final String stdout = checkFiles.stdout as String; - final List changedFiles = stdout.trim().split('\n'); - final Iterable changedMDFiles = - changedFiles.where((String filePath) => filePath.endsWith('.md')); - if (changedMDFiles.isNotEmpty) { - return 'Snippets are out of sync in the following files: ' - '${changedMDFiles.join(', ')}'; - } - - return null; - } -} diff --git a/script/tool/lib/src/update_release_info_command.dart b/script/tool/lib/src/update_release_info_command.dart deleted file mode 100644 index 8d7ceb84d31d..000000000000 --- a/script/tool/lib/src/update_release_info_command.dart +++ /dev/null @@ -1,313 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:yaml_edit/yaml_edit.dart'; - -import 'common/core.dart'; -import 'common/git_version_finder.dart'; -import 'common/package_looping_command.dart'; -import 'common/package_state_utils.dart'; -import 'common/repository_package.dart'; - -/// Supported version change types, from smallest to largest component. -enum _VersionIncrementType { build, bugfix, minor } - -/// Possible results of attempting to update a CHANGELOG.md file. -enum _ChangelogUpdateOutcome { addedSection, updatedSection, failed } - -/// A state machine for the process of updating a CHANGELOG.md. -enum _ChangelogUpdateState { - /// Looking for the first version section. - findingFirstSection, - - /// Looking for the first list entry in an existing section. - findingFirstListItem, - - /// Finished with updates. - finishedUpdating, -} - -/// A command to update the changelog, and optionally version, of packages. -class UpdateReleaseInfoCommand extends PackageLoopingCommand { - /// Creates a publish metadata updater command instance. - UpdateReleaseInfoCommand( - Directory packagesDir, { - GitDir? gitDir, - }) : super(packagesDir, gitDir: gitDir) { - argParser.addOption(_changelogFlag, - mandatory: true, - help: 'The changelog entry to add. ' - 'Each line will be a separate list entry.'); - argParser.addOption(_versionTypeFlag, - mandatory: true, - help: 'The version change level', - allowed: [ - _versionNext, - _versionMinimal, - _versionBugfix, - _versionMinor, - ], - allowedHelp: { - _versionNext: - 'No version change; just adds a NEXT entry to the changelog.', - _versionBugfix: 'Increments the bugfix version.', - _versionMinor: 'Increments the minor version.', - _versionMinimal: 'Depending on the changes to each package: ' - 'increments the bugfix version (for publishable changes), ' - "uses NEXT (for changes that don't need to be published), " - 'or skips (if no changes).', - }); - } - - static const String _changelogFlag = 'changelog'; - static const String _versionTypeFlag = 'version'; - - static const String _versionNext = 'next'; - static const String _versionBugfix = 'bugfix'; - static const String _versionMinor = 'minor'; - static const String _versionMinimal = 'minimal'; - - // The version change type, if there is a set type for all platforms. - // - // If null, either there is no version change, or it is dynamic (`minimal`). - _VersionIncrementType? _versionChange; - - // The cache of changed files, for dynamic version change determination. - // - // Only set for `minimal` version change. - late final List _changedFiles; - - @override - final String name = 'update-release-info'; - - @override - final String description = 'Updates CHANGELOG.md files, and optionally the ' - 'version in pubspec.yaml, in a way that is consistent with version-check ' - 'enforcement.'; - - @override - bool get hasLongOutput => false; - - @override - Future initializeRun() async { - if (getStringArg(_changelogFlag).trim().isEmpty) { - throw UsageException('Changelog message must not be empty.', usage); - } - switch (getStringArg(_versionTypeFlag)) { - case _versionMinor: - _versionChange = _VersionIncrementType.minor; - break; - case _versionBugfix: - _versionChange = _VersionIncrementType.bugfix; - break; - case _versionMinimal: - final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); - _changedFiles = await gitVersionFinder.getChangedFiles(); - // Anothing other than a fixed change is null. - _versionChange = null; - break; - case _versionNext: - _versionChange = null; - break; - default: - throw UnimplementedError('Unimplemented version change type'); - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - String nextVersionString; - - _VersionIncrementType? versionChange = _versionChange; - - // If the change type is `minimal` determine what changes, if any, are - // needed. - if (versionChange == null && - getStringArg(_versionTypeFlag) == _versionMinimal) { - final Directory gitRoot = - packagesDir.fileSystem.directory((await gitDir).path); - final String relativePackagePath = - getRelativePosixPath(package.directory, from: gitRoot); - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: _changedFiles, - relativePackagePath: relativePackagePath); - - if (!state.hasChanges) { - return PackageResult.skip('No changes to package'); - } - if (!state.needsVersionChange && !state.needsChangelogChange) { - return PackageResult.skip('No non-exempt changes to package'); - } - if (state.needsVersionChange) { - versionChange = _VersionIncrementType.bugfix; - } - } - - if (versionChange != null) { - final Version? updatedVersion = - _updatePubspecVersion(package, versionChange); - if (updatedVersion == null) { - return PackageResult.fail( - ['Could not determine current version.']); - } - nextVersionString = updatedVersion.toString(); - print('${indentation}Incremented version to $nextVersionString.'); - } else { - nextVersionString = 'NEXT'; - } - - final _ChangelogUpdateOutcome updateOutcome = - _updateChangelog(package, nextVersionString); - switch (updateOutcome) { - case _ChangelogUpdateOutcome.addedSection: - print('${indentation}Added a $nextVersionString section.'); - break; - case _ChangelogUpdateOutcome.updatedSection: - print('${indentation}Updated NEXT section.'); - break; - case _ChangelogUpdateOutcome.failed: - return PackageResult.fail(['Could not update CHANGELOG.md.']); - } - - return PackageResult.success(); - } - - _ChangelogUpdateOutcome _updateChangelog( - RepositoryPackage package, String version) { - if (!package.changelogFile.existsSync()) { - printError('${indentation}Missing CHANGELOG.md.'); - return _ChangelogUpdateOutcome.failed; - } - - final String newHeader = '## $version'; - final RegExp listItemPattern = RegExp(r'^(\s*[-*])'); - - final StringBuffer newChangelog = StringBuffer(); - _ChangelogUpdateState state = _ChangelogUpdateState.findingFirstSection; - bool updatedExistingSection = false; - - for (final String line in package.changelogFile.readAsLinesSync()) { - switch (state) { - case _ChangelogUpdateState.findingFirstSection: - final String trimmedLine = line.trim(); - if (trimmedLine.isEmpty) { - // Discard any whitespace at the top of the file. - } else if (trimmedLine == '## NEXT') { - // Replace the header with the new version (which may also be NEXT). - newChangelog.writeln(newHeader); - // Find the existing list to add to. - state = _ChangelogUpdateState.findingFirstListItem; - } else { - // The first content in the file isn't a NEXT section, so just add - // the new section. - [ - newHeader, - '', - ..._changelogAdditionsAsList(), - '', - line, // Don't drop the current line. - ].forEach(newChangelog.writeln); - state = _ChangelogUpdateState.finishedUpdating; - } - break; - case _ChangelogUpdateState.findingFirstListItem: - final RegExpMatch? match = listItemPattern.firstMatch(line); - if (match != null) { - final String listMarker = match[1]!; - // Add the new items on top. If the new change is changing the - // version, then the new item should be more relevant to package - // clients than anything that was already there. If it's still - // NEXT, the order doesn't matter. - [ - ..._changelogAdditionsAsList(listMarker: listMarker), - line, // Don't drop the current line. - ].forEach(newChangelog.writeln); - state = _ChangelogUpdateState.finishedUpdating; - updatedExistingSection = true; - } else if (line.trim().isEmpty) { - // Scan past empty lines, but keep them. - newChangelog.writeln(line); - } else { - printError(' Existing NEXT section has unrecognized format.'); - return _ChangelogUpdateOutcome.failed; - } - break; - case _ChangelogUpdateState.finishedUpdating: - // Once changes are done, add the rest of the lines as-is. - newChangelog.writeln(line); - break; - } - } - - package.changelogFile.writeAsStringSync(newChangelog.toString()); - - return updatedExistingSection - ? _ChangelogUpdateOutcome.updatedSection - : _ChangelogUpdateOutcome.addedSection; - } - - /// Returns the changelog to add as a Markdown list, using the given list - /// bullet style (default to the repository standard of '*'), and adding - /// any missing periods. - /// - /// E.g., 'A line\nAnother line.' will become: - /// ``` - /// [ '* A line.', '* Another line.' ] - /// ``` - Iterable _changelogAdditionsAsList({String listMarker = '*'}) { - return getStringArg(_changelogFlag).split('\n').map((String entry) { - String standardizedEntry = entry.trim(); - if (!standardizedEntry.endsWith('.')) { - standardizedEntry = '$standardizedEntry.'; - } - return '$listMarker $standardizedEntry'; - }); - } - - /// Updates the version in [package]'s pubspec according to [type], returning - /// the new version, or null if there was an error updating the version. - Version? _updatePubspecVersion( - RepositoryPackage package, _VersionIncrementType type) { - final Pubspec pubspec = package.parsePubspec(); - final Version? currentVersion = pubspec.version; - if (currentVersion == null) { - printError('${indentation}No version in pubspec.yaml'); - return null; - } - - // For versions less than 1.0, shift the change down one component per - // Dart versioning conventions. - final _VersionIncrementType adjustedType = currentVersion.major > 0 - ? type - : _VersionIncrementType.values[type.index - 1]; - - final Version newVersion = _nextVersion(currentVersion, adjustedType); - - // Write the new version to the pubspec. - final YamlEditor editablePubspec = - YamlEditor(package.pubspecFile.readAsStringSync()); - editablePubspec.update(['version'], newVersion.toString()); - package.pubspecFile.writeAsStringSync(editablePubspec.toString()); - - return newVersion; - } - - Version _nextVersion(Version version, _VersionIncrementType type) { - switch (type) { - case _VersionIncrementType.minor: - return version.nextMinor; - case _VersionIncrementType.bugfix: - return version.nextPatch; - case _VersionIncrementType.build: - final int buildNumber = - version.build.isEmpty ? 0 : version.build.first as int; - return Version(version.major, version.minor, version.patch, - build: '${buildNumber + 1}'); - } - } -} diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart deleted file mode 100644 index bb53620f06d3..000000000000 --- a/script/tool/lib/src/version_check_command.dart +++ /dev/null @@ -1,591 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:git/git.dart'; -import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:pub_semver/pub_semver.dart'; - -import 'common/core.dart'; -import 'common/git_version_finder.dart'; -import 'common/package_looping_command.dart'; -import 'common/package_state_utils.dart'; -import 'common/process_runner.dart'; -import 'common/pub_version_finder.dart'; -import 'common/repository_package.dart'; - -/// Categories of version change types. -enum NextVersionType { - /// A breaking change. - BREAKING_MAJOR, - - /// A minor change (e.g., added feature). - MINOR, - - /// A bugfix change. - PATCH, - - /// The release of an existing pre-1.0 version. - V1_RELEASE, -} - -/// The state of a package's version relative to the comparison base. -enum _CurrentVersionState { - /// The version is unchanged. - unchanged, - - /// The version has increased, and the transition is valid. - validIncrease, - - /// The version has decrease, and the transition is a valid revert. - validRevert, - - /// The version has changed, and the transition is invalid. - invalidChange, - - /// There was an error determining the version state. - unknown, -} - -/// Returns the set of allowed next non-prerelease versions, with their change -/// type, for [version]. -/// -/// [newVersion] is used to check whether this is a pre-1.0 version bump, as -/// those have different semver rules. -@visibleForTesting -Map getAllowedNextVersions( - Version version, { - required Version newVersion, -}) { - final Map allowedNextVersions = - { - version.nextMajor: NextVersionType.BREAKING_MAJOR, - version.nextMinor: NextVersionType.MINOR, - version.nextPatch: NextVersionType.PATCH, - }; - - if (version.major < 1 && newVersion.major < 1) { - int nextBuildNumber = -1; - if (version.build.isEmpty) { - nextBuildNumber = 1; - } else { - final int currentBuildNumber = version.build.first as int; - nextBuildNumber = currentBuildNumber + 1; - } - final Version nextBuildVersion = Version( - version.major, - version.minor, - version.patch, - build: nextBuildNumber.toString(), - ); - allowedNextVersions.clear(); - allowedNextVersions[version.nextMajor] = NextVersionType.V1_RELEASE; - allowedNextVersions[version.nextMinor] = NextVersionType.BREAKING_MAJOR; - allowedNextVersions[version.nextPatch] = NextVersionType.MINOR; - allowedNextVersions[nextBuildVersion] = NextVersionType.PATCH; - } - return allowedNextVersions; -} - -/// A command to validate version changes to packages. -class VersionCheckCommand extends PackageLoopingCommand { - /// Creates an instance of the version check command. - VersionCheckCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - GitDir? gitDir, - http.Client? httpClient, - }) : _pubVersionFinder = - PubVersionFinder(httpClient: httpClient ?? http.Client()), - super( - packagesDir, - processRunner: processRunner, - platform: platform, - gitDir: gitDir, - ) { - argParser.addFlag( - _againstPubFlag, - help: 'Whether the version check should run against the version on pub.\n' - 'Defaults to false, which means the version check only run against ' - 'the previous version in code.', - ); - argParser.addOption(_prLabelsArg, - help: 'A comma-separated list of labels associated with this PR, ' - 'if applicable.\n\n' - 'If supplied, this may be to allow overrides to some version ' - 'checks.'); - argParser.addFlag(_checkForMissingChanges, - help: 'Validates that changes to packages include CHANGELOG and ' - 'version changes unless they meet an established exemption.\n\n' - 'If used with --$_prLabelsArg, this is should only be ' - 'used in pre-submit CI checks, to prevent post-submit breakage ' - 'when labels are no longer applicable.', - hide: true); - argParser.addFlag(_ignorePlatformInterfaceBreaks, - help: 'Bypasses the check that platform interfaces do not contain ' - 'breaking changes.\n\n' - 'This is only intended for use in post-submit CI checks, to ' - 'prevent post-submit breakage when overriding the check with ' - 'labels. Pre-submit checks should always use ' - '--$_prLabelsArg instead.', - hide: true); - } - - static const String _againstPubFlag = 'against-pub'; - static const String _prLabelsArg = 'pr-labels'; - static const String _checkForMissingChanges = 'check-for-missing-changes'; - static const String _ignorePlatformInterfaceBreaks = - 'ignore-platform-interface-breaks'; - - /// The label that must be on a PR to allow a breaking - /// change to a platform interface. - static const String _breakingChangeOverrideLabel = - 'override: allow breaking change'; - - /// The label that must be on a PR to allow skipping a version change for a PR - /// that would normally require one. - static const String _missingVersionChangeOverrideLabel = - 'override: no versioning needed'; - - /// The label that must be on a PR to allow skipping a CHANGELOG change for a - /// PR that would normally require one. - static const String _missingChangelogChangeOverrideLabel = - 'override: no changelog needed'; - - final PubVersionFinder _pubVersionFinder; - - late final GitVersionFinder _gitVersionFinder; - late final String _mergeBase; - late final List _changedFiles; - - late final Set _prLabels = _getPRLabels(); - - @override - final String name = 'version-check'; - - @override - final String description = - 'Checks if the versions of packages have been incremented per pub specification.\n' - 'Also checks if the latest version in CHANGELOG matches the version in pubspec.\n\n' - 'This command requires "pub" and "flutter" to be in your path.'; - - @override - bool get hasLongOutput => false; - - @override - Future initializeRun() async { - _gitVersionFinder = await retrieveVersionFinder(); - _mergeBase = await _gitVersionFinder.getBaseSha(); - _changedFiles = await _gitVersionFinder.getChangedFiles(); - } - - @override - Future runForPackage(RepositoryPackage package) async { - final Pubspec? pubspec = _tryParsePubspec(package); - if (pubspec == null) { - // No remaining checks make sense, so fail immediately. - return PackageResult.fail(['Invalid pubspec.yaml.']); - } - - if (pubspec.publishTo == 'none') { - return PackageResult.skip('Found "publish_to: none".'); - } - - final Version? currentPubspecVersion = pubspec.version; - if (currentPubspecVersion == null) { - printError('${indentation}No version found in pubspec.yaml. A package ' - 'that intentionally has no version should be marked ' - '"publish_to: none".'); - // No remaining checks make sense, so fail immediately. - return PackageResult.fail(['No pubspec.yaml version.']); - } - - final List errors = []; - - bool versionChanged; - final _CurrentVersionState versionState = - await _getVersionState(package, pubspec: pubspec); - switch (versionState) { - case _CurrentVersionState.unchanged: - versionChanged = false; - break; - case _CurrentVersionState.validIncrease: - case _CurrentVersionState.validRevert: - versionChanged = true; - break; - case _CurrentVersionState.invalidChange: - versionChanged = true; - errors.add('Disallowed version change.'); - break; - case _CurrentVersionState.unknown: - versionChanged = false; - errors.add('Unable to determine previous version.'); - break; - } - - if (!(await _validateChangelogVersion(package, - pubspec: pubspec, pubspecVersionState: versionState))) { - errors.add('CHANGELOG.md failed validation.'); - } - - // If there are no other issues, make sure that there isn't a missing - // change to the version and/or CHANGELOG. - if (getBoolArg(_checkForMissingChanges) && - !versionChanged && - errors.isEmpty) { - final String? error = await _checkForMissingChangeError(package); - if (error != null) { - errors.add(error); - } - } - - return errors.isEmpty - ? PackageResult.success() - : PackageResult.fail(errors); - } - - @override - Future completeRun() async { - _pubVersionFinder.httpClient.close(); - } - - /// Returns the previous published version of [package]. - /// - /// [packageName] must be the actual name of the package as published (i.e., - /// the name from pubspec.yaml, not the on disk name if different.) - Future _fetchPreviousVersionFromPub(String packageName) async { - final PubVersionFinderResponse pubVersionFinderResponse = - await _pubVersionFinder.getPackageVersion(packageName: packageName); - switch (pubVersionFinderResponse.result) { - case PubVersionFinderResult.success: - return pubVersionFinderResponse.versions.first; - case PubVersionFinderResult.fail: - printError(''' -${indentation}Error fetching version on pub for $packageName. -${indentation}HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} -${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} -'''); - return null; - case PubVersionFinderResult.noPackageFound: - return Version.none; - } - } - - /// Returns the version of [package] from git at the base comparison hash. - Future _getPreviousVersionFromGit(RepositoryPackage package) async { - final File pubspecFile = package.pubspecFile; - final String relativePath = - path.relative(pubspecFile.absolute.path, from: (await gitDir).path); - // Use Posix-style paths for git. - final String gitPath = path.style == p.Style.windows - ? p.posix.joinAll(path.split(relativePath)) - : relativePath; - return _gitVersionFinder.getPackageVersion(gitPath, gitRef: _mergeBase); - } - - /// Returns the state of the verison of [package] relative to the comparison - /// base (git or pub, depending on flags). - Future<_CurrentVersionState> _getVersionState( - RepositoryPackage package, { - required Pubspec pubspec, - }) async { - // This method isn't called unless `version` is non-null. - final Version currentVersion = pubspec.version!; - Version? previousVersion; - String previousVersionSource; - if (getBoolArg(_againstPubFlag)) { - previousVersionSource = 'pub'; - previousVersion = await _fetchPreviousVersionFromPub(pubspec.name); - if (previousVersion == null) { - return _CurrentVersionState.unknown; - } - if (previousVersion != Version.none) { - print( - '$indentation${pubspec.name}: Current largest version on pub: $previousVersion'); - } - } else { - previousVersionSource = _mergeBase; - previousVersion = - await _getPreviousVersionFromGit(package) ?? Version.none; - } - if (previousVersion == Version.none) { - print('${indentation}Unable to find previous version ' - '${getBoolArg(_againstPubFlag) ? 'on pub server' : 'at git base'}.'); - logWarning( - '${indentation}If this package is not new, something has gone wrong.'); - return _CurrentVersionState.validIncrease; // Assume new, thus valid. - } - - if (previousVersion == currentVersion) { - print('${indentation}No version change.'); - return _CurrentVersionState.unchanged; - } - - // Check for reverts when doing local validation. - if (!getBoolArg(_againstPubFlag) && currentVersion < previousVersion) { - // Since this skips validation, try to ensure that it really is likely - // to be a revert rather than a typo by checking that the transition - // from the lower version to the new version would have been valid. - if (_shouldAllowVersionChange( - oldVersion: currentVersion, newVersion: previousVersion)) { - logWarning('${indentation}New version is lower than previous version. ' - 'This is assumed to be a revert.'); - return _CurrentVersionState.validRevert; - } - } - - final Map allowedNextVersions = - getAllowedNextVersions(previousVersion, newVersion: currentVersion); - - if (_shouldAllowVersionChange( - oldVersion: previousVersion, newVersion: currentVersion)) { - print('$indentation$previousVersion -> $currentVersion'); - } else { - printError('${indentation}Incorrectly updated version.\n' - '${indentation}HEAD: $currentVersion, $previousVersionSource: $previousVersion.\n' - '${indentation}Allowed versions: $allowedNextVersions'); - return _CurrentVersionState.invalidChange; - } - - // Check whether the version (or for a pre-release, the version that - // pre-release would eventually be released as) is a breaking change, and - // if so, validate it. - final Version targetReleaseVersion = - currentVersion.isPreRelease ? currentVersion.nextPatch : currentVersion; - if (allowedNextVersions[targetReleaseVersion] == - NextVersionType.BREAKING_MAJOR && - !_validateBreakingChange(package)) { - printError('${indentation}Breaking change detected.\n' - '${indentation}Breaking changes to platform interfaces are not ' - 'allowed without explicit justification.\n' - '${indentation}See ' - 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages ' - 'for more information.'); - return _CurrentVersionState.invalidChange; - } - - return _CurrentVersionState.validIncrease; - } - - /// Checks whether or not [package]'s CHANGELOG's versioning is correct, - /// both that it matches [pubspec] and that NEXT is used correctly, printing - /// the results of its checks. - /// - /// Returns false if the CHANGELOG fails validation. - Future _validateChangelogVersion( - RepositoryPackage package, { - required Pubspec pubspec, - required _CurrentVersionState pubspecVersionState, - }) async { - // This method isn't called unless `version` is non-null. - final Version fromPubspec = pubspec.version!; - - // get first version from CHANGELOG - final File changelog = package.changelogFile; - final List lines = changelog.readAsLinesSync(); - String? firstLineWithText; - final Iterator iterator = lines.iterator; - while (iterator.moveNext()) { - if (iterator.current.trim().isNotEmpty) { - firstLineWithText = iterator.current.trim(); - break; - } - } - // Remove all leading mark down syntax from the version line. - String? versionString = firstLineWithText?.split(' ').last; - - final String badNextErrorMessage = '${indentation}When bumping the version ' - 'for release, the NEXT section should be incorporated into the new ' - "version's release notes."; - - // Skip validation for the special NEXT version that's used to accumulate - // changes that don't warrant publishing on their own. - final bool hasNextSection = versionString == 'NEXT'; - if (hasNextSection) { - // NEXT should not be present in a commit that increases the version. - if (pubspecVersionState == _CurrentVersionState.validIncrease || - pubspecVersionState == _CurrentVersionState.invalidChange) { - printError(badNextErrorMessage); - return false; - } - print( - '${indentation}Found NEXT; validating next version in the CHANGELOG.'); - // Ensure that the version in pubspec hasn't changed without updating - // CHANGELOG. That means the next version entry in the CHANGELOG should - // pass the normal validation. - versionString = null; - while (iterator.moveNext()) { - if (iterator.current.trim().startsWith('## ')) { - versionString = iterator.current.trim().split(' ').last; - break; - } - } - } - - if (versionString == null) { - printError('${indentation}Unable to find a version in CHANGELOG.md'); - print('${indentation}The current version should be on a line starting ' - 'with "## ", either on the first non-empty line or after a "## NEXT" ' - 'section.'); - return false; - } - - final Version fromChangeLog; - try { - fromChangeLog = Version.parse(versionString); - } on FormatException { - printError('"$versionString" could not be parsed as a version.'); - return false; - } - - if (fromPubspec != fromChangeLog) { - printError(''' -${indentation}Versions in CHANGELOG.md and pubspec.yaml do not match. -${indentation}The version in pubspec.yaml is $fromPubspec. -${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. -'''); - return false; - } - - // If NEXT wasn't the first section, it should not exist at all. - if (!hasNextSection) { - final RegExp nextRegex = RegExp(r'^#+\s*NEXT\s*$'); - if (lines.any((String line) => nextRegex.hasMatch(line))) { - printError(badNextErrorMessage); - return false; - } - } - - return true; - } - - Pubspec? _tryParsePubspec(RepositoryPackage package) { - try { - final Pubspec pubspec = package.parsePubspec(); - return pubspec; - } on Exception catch (exception) { - printError('${indentation}Failed to parse `pubspec.yaml`: $exception}'); - return null; - } - } - - /// Checks whether the current breaking change to [package] should be allowed, - /// logging extra information for auditing when allowing unusual cases. - bool _validateBreakingChange(RepositoryPackage package) { - // Only platform interfaces have breaking change restrictions. - if (!package.isPlatformInterface) { - return true; - } - - if (getBoolArg(_ignorePlatformInterfaceBreaks)) { - logWarning( - '${indentation}Allowing breaking change to ${package.displayName} ' - 'due to --$_ignorePlatformInterfaceBreaks'); - return true; - } - - if (_prLabels.contains(_breakingChangeOverrideLabel)) { - logWarning( - '${indentation}Allowing breaking change to ${package.displayName} ' - 'due to the "$_breakingChangeOverrideLabel" label.'); - return true; - } - - return false; - } - - /// Returns the labels associated with this PR, if any, or an empty set - /// if that flag is not provided. - Set _getPRLabels() { - final String labels = getStringArg(_prLabelsArg); - if (labels.isEmpty) { - return {}; - } - return labels.split(',').map((String label) => label.trim()).toSet(); - } - - /// Returns true if the given version transition should be allowed. - bool _shouldAllowVersionChange( - {required Version oldVersion, required Version newVersion}) { - // Get the non-pre-release next version mapping. - final Map allowedNextVersions = - getAllowedNextVersions(oldVersion, newVersion: newVersion); - - if (allowedNextVersions.containsKey(newVersion)) { - return true; - } - // Allow a pre-release version of a version that would be a valid - // transition. - if (newVersion.isPreRelease) { - final Version targetReleaseVersion = newVersion.nextPatch; - if (allowedNextVersions.containsKey(targetReleaseVersion)) { - return true; - } - } - return false; - } - - /// Returns an error string if the changes to this package should have - /// resulted in a version change, or shoud have resulted in a CHANGELOG change - /// but didn't. - /// - /// This should only be called if the version did not change. - Future _checkForMissingChangeError(RepositoryPackage package) async { - // Find the relative path to the current package, as it would appear at the - // beginning of a path reported by getChangedFiles() (which always uses - // Posix paths). - final Directory gitRoot = - packagesDir.fileSystem.directory((await gitDir).path); - final String relativePackagePath = - getRelativePosixPath(package.directory, from: gitRoot); - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: _changedFiles, - relativePackagePath: relativePackagePath, - git: await retrieveVersionFinder()); - - if (!state.hasChanges) { - return null; - } - - if (state.needsVersionChange) { - if (_prLabels.contains(_missingVersionChangeOverrideLabel)) { - logWarning('Ignoring lack of version change due to the ' - '"$_missingVersionChangeOverrideLabel" label.'); - } else { - printError( - 'No version change found, but the change to this package could ' - 'not be verified to be exempt from version changes according to ' - 'repository policy. If this is a false positive, please comment in ' - 'the PR to explain why the PR is exempt, and add (or ask your ' - 'reviewer to add) the "$_missingVersionChangeOverrideLabel" ' - 'label.'); - return 'Missing version change'; - } - } - - if (!state.hasChangelogChange && state.needsChangelogChange) { - if (_prLabels.contains(_missingChangelogChangeOverrideLabel)) { - logWarning('Ignoring lack of CHANGELOG update due to the ' - '"$_missingChangelogChangeOverrideLabel" label.'); - } else { - printError( - 'No CHANGELOG change found. If this PR needs an exemption from ' - 'the standard policy of listing all changes in the CHANGELOG, ' - 'comment in the PR to explain why the PR is exempt, and add (or ' - 'ask your reviewer to add) the ' - '"$_missingChangelogChangeOverrideLabel" label. Otherwise, ' - 'please add a NEXT entry in the CHANGELOG as described in ' - 'the contributing guide.'); - return 'Missing CHANGELOG change'; - } - } - - return null; - } -} diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart deleted file mode 100644 index a81bf15477af..000000000000 --- a/script/tool/lib/src/xcode_analyze_command.dart +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:platform/platform.dart'; - -import 'common/core.dart'; -import 'common/package_looping_command.dart'; -import 'common/plugin_utils.dart'; -import 'common/process_runner.dart'; -import 'common/repository_package.dart'; -import 'common/xcode.dart'; - -/// The command to run Xcode's static analyzer on plugins. -class XcodeAnalyzeCommand extends PackageLoopingCommand { - /// Creates an instance of the test command. - XcodeAnalyzeCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - }) : _xcode = Xcode(processRunner: processRunner, log: true), - super(packagesDir, processRunner: processRunner, platform: platform) { - argParser.addFlag(platformIOS, help: 'Analyze iOS'); - argParser.addFlag(platformMacOS, help: 'Analyze macOS'); - argParser.addOption(_minIOSVersionArg, - help: 'Sets the minimum iOS deployment version to use when compiling, ' - 'overriding the default minimum version. This can be used to find ' - 'deprecation warnings that will affect the plugin in the future.'); - argParser.addOption(_minMacOSVersionArg, - help: - 'Sets the minimum macOS deployment version to use when compiling, ' - 'overriding the default minimum version. This can be used to find ' - 'deprecation warnings that will affect the plugin in the future.'); - } - - static const String _minIOSVersionArg = 'ios-min-version'; - static const String _minMacOSVersionArg = 'macos-min-version'; - - final Xcode _xcode; - - @override - final String name = 'xcode-analyze'; - - @override - final String description = - 'Runs Xcode analysis on the iOS and/or macOS example apps.'; - - @override - Future initializeRun() async { - if (!(getBoolArg(platformIOS) || getBoolArg(platformMacOS))) { - printError('At least one platform flag must be provided.'); - throw ToolExit(exitInvalidArguments); - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - final bool testIOS = getBoolArg(platformIOS) && - pluginSupportsPlatform(platformIOS, package, - requiredMode: PlatformSupport.inline); - final bool testMacOS = getBoolArg(platformMacOS) && - pluginSupportsPlatform(platformMacOS, package, - requiredMode: PlatformSupport.inline); - - final bool multiplePlatformsRequested = - getBoolArg(platformIOS) && getBoolArg(platformMacOS); - if (!(testIOS || testMacOS)) { - return PackageResult.skip('Not implemented for target platform(s).'); - } - - final String minIOSVersion = getStringArg(_minIOSVersionArg); - final String minMacOSVersion = getStringArg(_minMacOSVersionArg); - - final List failures = []; - if (testIOS && - !await _analyzePlugin(package, 'iOS', extraFlags: [ - '-destination', - 'generic/platform=iOS Simulator', - if (minIOSVersion.isNotEmpty) - 'IPHONEOS_DEPLOYMENT_TARGET=$minIOSVersion', - ])) { - failures.add('iOS'); - } - if (testMacOS && - !await _analyzePlugin(package, 'macOS', extraFlags: [ - if (minMacOSVersion.isNotEmpty) - 'MACOSX_DEPLOYMENT_TARGET=$minMacOSVersion', - ])) { - failures.add('macOS'); - } - - // Only provide the failing platform in the failure details if testing - // multiple platforms, otherwise it's just noise. - return failures.isEmpty - ? PackageResult.success() - : PackageResult.fail( - multiplePlatformsRequested ? failures : []); - } - - /// Analyzes [plugin] for [platform], returning true if it passed analysis. - Future _analyzePlugin( - RepositoryPackage plugin, - String platform, { - List extraFlags = const [], - }) async { - bool passing = true; - for (final RepositoryPackage example in plugin.getExamples()) { - // Running tests and static analyzer. - final String examplePath = getRelativePosixPath(example.directory, - from: plugin.directory.parent); - print('Running $platform tests and analyzer for $examplePath...'); - final int exitCode = await _xcode.runXcodeBuild( - example.directory, - actions: ['analyze'], - workspace: '${platform.toLowerCase()}/Runner.xcworkspace', - scheme: 'Runner', - configuration: 'Debug', - extraFlags: [ - ...extraFlags, - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - ); - if (exitCode == 0) { - printSuccess('$examplePath ($platform) passed analysis.'); - } else { - printError('$examplePath ($platform) failed analysis.'); - passing = false; - } - } - return passing; - } -} diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index abf2a61f4cf0..60884fdeb8ae 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,8 @@ name: flutter_plugin_tools description: Productivity utils for flutter/plugins and flutter/packages repository: https://github.com/flutter/plugins/tree/main/script/tool -version: 0.13.3 +version: 0.13.4+2 +publish_to: none # See README.md dependencies: args: ^2.1.0 diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart deleted file mode 100644 index e6b910960846..000000000000 --- a/script/tool/test/analyze_command_test.dart +++ /dev/null @@ -1,425 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/analyze_command.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final AnalyzeCommand analyzeCommand = AnalyzeCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner('analyze_command', 'Test for analyze_command'); - runner.addCommand(analyzeCommand); - }); - - test('analyzes all packages', () async { - final RepositoryPackage package1 = createFakePackage('a', packagesDir); - final RepositoryPackage plugin2 = createFakePlugin('b', packagesDir); - - await runCapturingPrint(runner, ['analyze']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], package1.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - package1.path), - ProcessCall('flutter', const ['pub', 'get'], plugin2.path), - ProcessCall( - 'dart', const ['analyze', '--fatal-infos'], plugin2.path), - ])); - }); - - test('skips flutter pub get for examples', () async { - final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); - - await runCapturingPrint(runner, ['analyze']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], plugin1.path), - ProcessCall( - 'dart', const ['analyze', '--fatal-infos'], plugin1.path), - ])); - }); - - test('runs flutter pub get for non-example subpackages', () async { - final RepositoryPackage mainPackage = createFakePackage('a', packagesDir); - final Directory otherPackagesDir = - mainPackage.directory.childDirectory('other_packages'); - final RepositoryPackage subpackage1 = - createFakePackage('subpackage1', otherPackagesDir); - final RepositoryPackage subpackage2 = - createFakePackage('subpackage2', otherPackagesDir); - - await runCapturingPrint(runner, ['analyze']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', const ['pub', 'get'], mainPackage.path), - ProcessCall( - 'flutter', const ['pub', 'get'], subpackage1.path), - ProcessCall( - 'flutter', const ['pub', 'get'], subpackage2.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - mainPackage.path), - ])); - }); - - test('passes lib/ directory with --lib-only', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - await runCapturingPrint(runner, ['analyze', '--lib-only']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], package.path), - ProcessCall('dart', const ['analyze', '--fatal-infos', 'lib'], - package.path), - ])); - }); - - test('skips when missing lib/ directory with --lib-only', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - package.libDirectory.deleteSync(); - - final List output = - await runCapturingPrint(runner, ['analyze', '--lib-only']); - - expect(processRunner.recordedCalls, isEmpty); - expect( - output, - containsAllInOrder([ - contains('SKIPPING: No lib/ directory'), - ]), - ); - }); - - test( - 'does not run flutter pub get for non-example subpackages with --lib-only', - () async { - final RepositoryPackage mainPackage = createFakePackage('a', packagesDir); - final Directory otherPackagesDir = - mainPackage.directory.childDirectory('other_packages'); - createFakePackage('subpackage1', otherPackagesDir); - createFakePackage('subpackage2', otherPackagesDir); - - await runCapturingPrint(runner, ['analyze', '--lib-only']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', const ['pub', 'get'], mainPackage.path), - ProcessCall('dart', const ['analyze', '--fatal-infos', 'lib'], - mainPackage.path), - ])); - }); - - test("don't elide a non-contained example package", () async { - final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); - final RepositoryPackage plugin2 = createFakePlugin('example', packagesDir); - - await runCapturingPrint(runner, ['analyze']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], plugin1.path), - ProcessCall( - 'dart', const ['analyze', '--fatal-infos'], plugin1.path), - ProcessCall('flutter', const ['pub', 'get'], plugin2.path), - ProcessCall( - 'dart', const ['analyze', '--fatal-infos'], plugin2.path), - ])); - }); - - test('uses a separate analysis sdk', () async { - final RepositoryPackage plugin = createFakePlugin('a', packagesDir); - - await runCapturingPrint( - runner, ['analyze', '--analysis-sdk', 'foo/bar/baz']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - const ['pub', 'get'], - plugin.path, - ), - ProcessCall( - 'foo/bar/baz/bin/dart', - const ['analyze', '--fatal-infos'], - plugin.path, - ), - ]), - ); - }); - - test('downgrades first when requested', () async { - final RepositoryPackage plugin = createFakePlugin('a', packagesDir); - - await runCapturingPrint(runner, ['analyze', '--downgrade']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - const ['pub', 'downgrade'], - plugin.path, - ), - ProcessCall( - 'flutter', - const ['pub', 'get'], - plugin.path, - ), - ProcessCall( - 'dart', - const ['analyze', '--fatal-infos'], - plugin.path, - ), - ]), - ); - }); - - group('verifies analysis settings', () { - test('fails analysis_options.yaml', () async { - createFakePlugin('foo', packagesDir, - extraFiles: ['analysis_options.yaml']); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['analyze'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Found an extra analysis_options.yaml at /packages/foo/analysis_options.yaml'), - contains(' foo:\n' - ' Unexpected local analysis options'), - ]), - ); - }); - - test('fails .analysis_options', () async { - createFakePlugin('foo', packagesDir, - extraFiles: ['.analysis_options']); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['analyze'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Found an extra analysis_options.yaml at /packages/foo/.analysis_options'), - contains(' foo:\n' - ' Unexpected local analysis options'), - ]), - ); - }); - - test('takes an allow list', () async { - final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, - extraFiles: ['analysis_options.yaml']); - - await runCapturingPrint( - runner, ['analyze', '--custom-analysis', 'foo']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], plugin.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin.path), - ])); - }); - - test('takes an allow config file', () async { - final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, - extraFiles: ['analysis_options.yaml']); - final File allowFile = packagesDir.childFile('custom.yaml'); - allowFile.writeAsStringSync('- foo'); - - await runCapturingPrint( - runner, ['analyze', '--custom-analysis', allowFile.path]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], plugin.path), - ProcessCall('dart', const ['analyze', '--fatal-infos'], - plugin.path), - ])); - }); - - test('allows an empty config file', () async { - createFakePlugin('foo', packagesDir, - extraFiles: ['analysis_options.yaml']); - final File allowFile = packagesDir.childFile('custom.yaml'); - allowFile.createSync(); - - await expectLater( - () => runCapturingPrint( - runner, ['analyze', '--custom-analysis', allowFile.path]), - throwsA(isA())); - }); - - // See: https://github.com/flutter/flutter/issues/78994 - test('takes an empty allow list', () async { - createFakePlugin('foo', packagesDir, - extraFiles: ['analysis_options.yaml']); - - await expectLater( - () => runCapturingPrint( - runner, ['analyze', '--custom-analysis', '']), - throwsA(isA())); - }); - }); - - test('fails if "pub get" fails', () async { - createFakePlugin('foo', packagesDir); - - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(exitCode: 1) // flutter pub get - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['analyze'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to get dependencies'), - ]), - ); - }); - - test('fails if "pub downgrade" fails', () async { - createFakePlugin('foo', packagesDir); - - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(exitCode: 1) // flutter pub downgrade - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['analyze', '--downgrade'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to downgrade dependencies'), - ]), - ); - }); - - test('fails if "analyze" fails', () async { - createFakePlugin('foo', packagesDir); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1) // dart analyze - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['analyze'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' foo'), - ]), - ); - }); - - // Ensure that the command used to analyze flutter/plugins in the Dart repo: - // https://github.com/dart-lang/sdk/blob/main/tools/bots/flutter/analyze_flutter_plugins.sh - // continues to work. - // - // DO NOT remove or modify this test without a coordination plan in place to - // modify the script above, as it is run from source, but out-of-repo. - // Contact stuartmorgan or devoncarew for assistance. - test('Dart repo analyze command works', () async { - final RepositoryPackage plugin = createFakePlugin('foo', packagesDir, - extraFiles: ['analysis_options.yaml']); - final File allowFile = packagesDir.childFile('custom.yaml'); - allowFile.writeAsStringSync('- foo'); - - await runCapturingPrint(runner, [ - // DO NOT change this call; see comment above. - 'analyze', - '--analysis-sdk', - 'foo/bar/baz', - '--custom-analysis', - allowFile.path - ]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - const ['pub', 'get'], - plugin.path, - ), - ProcessCall( - 'foo/bar/baz/bin/dart', - const ['analyze', '--fatal-infos'], - plugin.path, - ), - ]), - ); - }); -} diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart deleted file mode 100644 index a819e7a12674..000000000000 --- a/script/tool/test/build_examples_command_test.dart +++ /dev/null @@ -1,634 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/build_examples_command.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('build-example', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final BuildExamplesCommand command = BuildExamplesCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'build_examples_command', 'Test for build_example_command'); - runner.addCommand(command); - }); - - test('fails if no plaform flags are passed', () async { - Error? commandError; - final List output = await runCapturingPrint( - runner, ['build-examples'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('At least one platform must be provided'), - ])); - }); - - test('fails if building fails', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline), - }); - - processRunner - .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [ - MockProcess(exitCode: 1) // flutter pub get - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['build-examples', '--ios'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin:\n' - ' plugin/example (iOS)'), - ])); - }); - - test('fails if a plugin has no examples', () async { - createFakePlugin('plugin', packagesDir, - examples: [], - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - processRunner - .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [ - MockProcess(exitCode: 1) // flutter pub get - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['build-examples', '--ios'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin:\n' - ' No examples found'), - ])); - }); - - test('building for iOS when plugin is not set up for iOS results in no-op', - () async { - mockPlatform.isMacOS = true; - createFakePlugin('plugin', packagesDir); - - final List output = - await runCapturingPrint(runner, ['build-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('iOS is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for iOS', () async { - mockPlatform.isMacOS = true; - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, - ['build-examples', '--ios', '--enable-experiment=exp1']); - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for iOS', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'build', - 'ios', - '--no-codesign', - '--enable-experiment=exp1' - ], - pluginExampleDirectory.path), - ])); - }); - - test( - 'building for Linux when plugin is not set up for Linux results in no-op', - () async { - mockPlatform.isLinux = true; - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--linux']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Linux is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --linux with no - // Linux implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for Linux', () async { - mockPlatform.isLinux = true; - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--linux']); - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for Linux', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'linux'], pluginExampleDirectory.path), - ])); - }); - - test('building for macOS with no implementation results in no-op', - () async { - mockPlatform.isMacOS = true; - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--macos']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('macOS is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for macOS', () async { - mockPlatform.isMacOS = true; - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--macos']); - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for macOS', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'macos'], pluginExampleDirectory.path), - ])); - }); - - test('building for web with no implementation results in no-op', () async { - createFakePlugin('plugin', packagesDir); - - final List output = - await runCapturingPrint(runner, ['build-examples', '--web']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('web is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for web', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = - await runCapturingPrint(runner, ['build-examples', '--web']); - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for web', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'web'], pluginExampleDirectory.path), - ])); - }); - - test( - 'building for Windows when plugin is not set up for Windows results in no-op', - () async { - mockPlatform.isWindows = true; - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--windows']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Windows is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --windows with no - // Windows implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for Windows', () async { - mockPlatform.isWindows = true; - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--windows']); - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for Windows', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['build', 'windows'], - pluginExampleDirectory.path), - ])); - }); - - test( - 'building for Android when plugin is not set up for Android results in no-op', - () async { - createFakePlugin('plugin', packagesDir); - - final List output = - await runCapturingPrint(runner, ['build-examples', '--apk']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Android is not supported by this plugin'), - ]), - ); - - // Output should be empty since running build-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('building for Android', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'build-examples', - '--apk', - ]); - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for Android (apk)', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['build', 'apk'], pluginExampleDirectory.path), - ])); - }); - - test('enable-experiment flag for Android', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - await runCapturingPrint(runner, - ['build-examples', '--apk', '--enable-experiment=exp1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['build', 'apk', '--enable-experiment=exp1'], - pluginExampleDirectory.path), - ])); - }); - - test('enable-experiment flag for ios', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - await runCapturingPrint(runner, - ['build-examples', '--ios', '--enable-experiment=exp1']); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'build', - 'ios', - '--no-codesign', - '--enable-experiment=exp1' - ], - pluginExampleDirectory.path), - ])); - }); - - test('logs skipped platforms', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - }); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--apk', '--ios', '--macos']); - - expect( - output, - containsAllInOrder([ - contains('Skipping unsupported platform(s): iOS, macOS'), - ]), - ); - }); - - group('packages', () { - test('builds when requested platform is supported by example', () async { - final RepositoryPackage package = createFakePackage( - 'package', packagesDir, isFlutter: true, extraFiles: [ - 'example/ios/Runner.xcodeproj/project.pbxproj' - ]); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for package'), - contains('BUILDING package/example for iOS'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'build', - 'ios', - '--no-codesign', - ], - getExampleDir(package).path), - ])); - }); - - test('skips non-Flutter examples', () async { - createFakePackage('package', packagesDir); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for package'), - contains('No examples found supporting requested platform(s).'), - ]), - ); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skips when there is no example', () async { - createFakePackage('package', packagesDir, - isFlutter: true, examples: []); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for package'), - contains('No examples found supporting requested platform(s).'), - ]), - ); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip when example does not support requested platform', () async { - createFakePackage('package', packagesDir, - isFlutter: true, - extraFiles: ['example/linux/CMakeLists.txt']); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for package'), - contains('Skipping iOS for package/example; not supported.'), - contains('No examples found supporting requested platform(s).'), - ]), - ); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('logs skipped platforms when only some are supported', () async { - final RepositoryPackage package = createFakePackage( - 'package', packagesDir, - isFlutter: true, - extraFiles: ['example/linux/CMakeLists.txt']); - - final List output = await runCapturingPrint( - runner, ['build-examples', '--apk', '--linux']); - - expect( - output, - containsAllInOrder([ - contains('Running for package'), - contains('Building for: Android, Linux'), - contains('Skipping Android for package/example; not supported.'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['build', 'linux'], - getExampleDir(package).path), - ])); - }); - }); - - test('The .pluginToolsConfig.yaml file', () async { - mockPlatform.isLinux = true; - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final File pluginExampleConfigFile = - pluginExampleDirectory.childFile('.pluginToolsConfig.yaml'); - pluginExampleConfigFile - .writeAsStringSync('buildFlags:\n global:\n - "test argument"'); - - final List output = [ - ...await runCapturingPrint( - runner, ['build-examples', '--linux']), - ...await runCapturingPrint( - runner, ['build-examples', '--macos']), - ]; - - expect( - output, - containsAllInOrder([ - '\nBUILDING plugin/example for Linux', - '\nBUILDING plugin/example for macOS', - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['build', 'linux', 'test argument'], - pluginExampleDirectory.path), - ProcessCall( - getFlutterCommand(mockPlatform), - const ['build', 'macos', 'test argument'], - pluginExampleDirectory.path), - ])); - }); - }); -} diff --git a/script/tool/test/common/file_utils_test.dart b/script/tool/test/common/file_utils_test.dart deleted file mode 100644 index 79b804e31ea5..000000000000 --- a/script/tool/test/common/file_utils_test.dart +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/file_utils.dart'; -import 'package:test/test.dart'; - -void main() { - test('works on Posix', () async { - final FileSystem fileSystem = - MemoryFileSystem(); - - final Directory base = fileSystem.directory('/').childDirectory('base'); - final File file = - childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); - - expect(file.absolute.path, '/base/foo/bar/baz.txt'); - }); - - test('works on Windows', () async { - final FileSystem fileSystem = - MemoryFileSystem(style: FileSystemStyle.windows); - - final Directory base = fileSystem.directory(r'C:\').childDirectory('base'); - final File file = - childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); - - expect(file.absolute.path, r'C:\base\foo\bar\baz.txt'); - }); -} diff --git a/script/tool/test/common/git_version_finder_test.dart b/script/tool/test/common/git_version_finder_test.dart deleted file mode 100644 index 538b72a90021..000000000000 --- a/script/tool/test/common/git_version_finder_test.dart +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -import 'package:flutter_plugin_tools/src/common/git_version_finder.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'package_command_test.mocks.dart'; - -void main() { - late List?> gitDirCommands; - late String gitDiffResponse; - late MockGitDir gitDir; - String mergeBaseResponse = ''; - - setUp(() { - gitDirCommands = ?>[]; - gitDiffResponse = ''; - gitDir = MockGitDir(); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - gitDirCommands.add(arguments); - final MockProcessResult mockProcessResult = MockProcessResult(); - if (arguments[0] == 'diff') { - when(mockProcessResult.stdout as String?) - .thenReturn(gitDiffResponse); - } else if (arguments[0] == 'merge-base') { - when(mockProcessResult.stdout as String?) - .thenReturn(mergeBaseResponse); - } - return Future.value(mockProcessResult); - }); - }); - - test('No git diff should result no files changed', () async { - final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - final List changedFiles = await finder.getChangedFiles(); - - expect(changedFiles, isEmpty); - }); - - test('get correct files changed based on git diff', () async { - gitDiffResponse = ''' -file1/file1.cc -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - final List changedFiles = await finder.getChangedFiles(); - - expect(changedFiles, equals(['file1/file1.cc', 'file2/file2.cc'])); - }); - - test('get correct pubspec change based on git diff', () async { - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); - final List changedFiles = await finder.getChangedPubSpecs(); - - expect(changedFiles, equals(['file1/pubspec.yaml'])); - }); - - test('use correct base sha if not specified', () async { - mergeBaseResponse = 'shaqwiueroaaidf12312jnadf123nd'; - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - - final GitVersionFinder finder = GitVersionFinder(gitDir, null); - await finder.getChangedFiles(); - verify(gitDir.runCommand( - ['diff', '--name-only', mergeBaseResponse, 'HEAD'])); - }); - - test('use correct base sha if specified', () async { - const String customBaseSha = 'aklsjdcaskf12312'; - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); - await finder.getChangedFiles(); - verify(gitDir - .runCommand(['diff', '--name-only', customBaseSha, 'HEAD'])); - }); - - test('include uncommitted files if requested', () async { - const String customBaseSha = 'aklsjdcaskf12312'; - gitDiffResponse = ''' -file1/pubspec.yaml -file2/file2.cc -'''; - final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); - await finder.getChangedFiles(includeUncommitted: true); - // The call should not have HEAD as a final argument like the default diff. - verify(gitDir.runCommand(['diff', '--name-only', customBaseSha])); - }); -} - -class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart deleted file mode 100644 index 8df4a65b93a5..000000000000 --- a/script/tool/test/common/gradle_test.dart +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/gradle.dart'; -import 'package:test/test.dart'; - -import '../mocks.dart'; -import '../util.dart'; - -void main() { - late FileSystem fileSystem; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - processRunner = RecordingProcessRunner(); - }); - - group('isConfigured', () { - test('reports true when configured on Windows', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/gradlew.bat']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isWindows: true), - ); - - expect(project.isConfigured(), true); - }); - - test('reports true when configured on non-Windows', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/gradlew']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isMacOS: true), - ); - - expect(project.isConfigured(), true); - }); - - test('reports false when not configured on Windows', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/foo']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isWindows: true), - ); - - expect(project.isConfigured(), false); - }); - - test('reports true when configured on non-Windows', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/foo']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isMacOS: true), - ); - - expect(project.isConfigured(), false); - }); - }); - - group('runCommand', () { - test('runs without arguments', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/gradlew']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isMacOS: true), - ); - - final int exitCode = await project.runCommand('foo'); - - expect(exitCode, 0); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - plugin - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path, - const [ - 'foo', - ], - plugin.platformDirectory(FlutterPlatform.android).path), - ])); - }); - - test('runs with arguments', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/gradlew']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isMacOS: true), - ); - - final int exitCode = await project.runCommand( - 'foo', - arguments: ['--bar', '--baz'], - ); - - expect(exitCode, 0); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - plugin - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path, - const [ - 'foo', - '--bar', - '--baz', - ], - plugin.platformDirectory(FlutterPlatform.android).path), - ])); - }); - - test('runs with the correct wrapper on Windows', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/gradlew.bat']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isWindows: true), - ); - - final int exitCode = await project.runCommand('foo'); - - expect(exitCode, 0); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - plugin - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew.bat') - .path, - const [ - 'foo', - ], - plugin.platformDirectory(FlutterPlatform.android).path), - ])); - }); - - test('returns error codes', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', fileSystem.directory('/'), - extraFiles: ['android/gradlew.bat']); - final GradleProject project = GradleProject( - plugin, - processRunner: processRunner, - platform: MockPlatform(isWindows: true), - ); - - processRunner.mockProcessesForExecutable[project.gradleWrapper.path] = - [ - MockProcess(exitCode: 1), - ]; - - final int exitCode = await project.runCommand('foo'); - - expect(exitCode, 1); - }); - }); -} diff --git a/script/tool/test/common/package_command_test.dart b/script/tool/test/common/package_command_test.dart deleted file mode 100644 index aa0a20253955..000000000000 --- a/script/tool/test/common/package_command_test.dart +++ /dev/null @@ -1,1069 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/package_command.dart'; -import 'package:flutter_plugin_tools/src/common/process_runner.dart'; -import 'package:git/git.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import '../mocks.dart'; -import '../util.dart'; -import 'package_command_test.mocks.dart'; - -@GenerateMocks([GitDir]) -void main() { - late RecordingProcessRunner processRunner; - late SamplePackageCommand command; - late CommandRunner runner; - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late Directory thirdPartyPackagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - thirdPartyPackagesDir = packagesDir.parent - .childDirectory('third_party') - .childDirectory('packages'); - - final MockGitDir gitDir = MockGitDir(); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - // Attach the first argument to the command to make targeting the mock - // results easier. - final String gitCommand = arguments.removeAt(0); - return processRunner.run('git-$gitCommand', arguments); - }); - processRunner = RecordingProcessRunner(); - command = SamplePackageCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: gitDir, - ); - runner = - CommandRunner('common_command', 'Test for common functionality'); - runner.addCommand(command); - }); - - group('plugin iteration', () { - test('all plugins from file system', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, ['sample']); - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - }); - - test('includes both plugins and packages', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final RepositoryPackage package3 = - createFakePackage('package3', packagesDir); - final RepositoryPackage package4 = - createFakePackage('package4', packagesDir); - await runCapturingPrint(runner, ['sample']); - expect( - command.plugins, - unorderedEquals([ - plugin1.path, - plugin2.path, - package3.path, - package4.path, - ])); - }); - - test('includes packages without source', () async { - final RepositoryPackage package = - createFakePackage('package', packagesDir); - package.libDirectory.deleteSync(recursive: true); - - await runCapturingPrint(runner, ['sample']); - expect( - command.plugins, - unorderedEquals([ - package.path, - ])); - }); - - test('all plugins includes third_party/packages', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final RepositoryPackage plugin3 = - createFakePlugin('plugin3', thirdPartyPackagesDir); - await runCapturingPrint(runner, ['sample']); - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); - }); - - test('--packages limits packages', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - createFakePackage('package3', packagesDir); - final RepositoryPackage package4 = - createFakePackage('package4', packagesDir); - await runCapturingPrint( - runner, ['sample', '--packages=plugin1,package4']); - expect( - command.plugins, - unorderedEquals([ - plugin1.path, - package4.path, - ])); - }); - - test('--plugins acts as an alias to --packages', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - createFakePackage('package3', packagesDir); - final RepositoryPackage package4 = - createFakePackage('package4', packagesDir); - await runCapturingPrint( - runner, ['sample', '--plugins=plugin1,package4']); - expect( - command.plugins, - unorderedEquals([ - plugin1.path, - package4.path, - ])); - }); - - test('exclude packages when packages flag is specified', () async { - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--packages=plugin1,plugin2', - '--exclude=plugin1' - ]); - expect(command.plugins, unorderedEquals([plugin2.path])); - }); - - test("exclude packages when packages flag isn't specified", () async { - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint( - runner, ['sample', '--exclude=plugin1,plugin2']); - expect(command.plugins, unorderedEquals([])); - }); - - test('exclude federated plugins when packages flag is specified', () async { - createFakePlugin('plugin1', packagesDir.childDirectory('federated')); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--packages=federated/plugin1,plugin2', - '--exclude=federated/plugin1' - ]); - expect(command.plugins, unorderedEquals([plugin2.path])); - }); - - test('exclude entire federated plugins when packages flag is specified', - () async { - createFakePlugin('plugin1', packagesDir.childDirectory('federated')); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--packages=federated/plugin1,plugin2', - '--exclude=federated' - ]); - expect(command.plugins, unorderedEquals([plugin2.path])); - }); - - test('exclude accepts config files', () async { - createFakePlugin('plugin1', packagesDir); - final File configFile = packagesDir.childFile('exclude.yaml'); - configFile.writeAsStringSync('- plugin1'); - - await runCapturingPrint(runner, [ - 'sample', - '--packages=plugin1', - '--exclude=${configFile.path}' - ]); - expect(command.plugins, unorderedEquals([])); - }); - - test( - 'explicitly specifying the plugin (group) name of a federated plugin ' - 'should include all plugins in the group', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1/plugin1.dart -'''), - ]; - final Directory pluginGroup = packagesDir.childDirectory('plugin1'); - final RepositoryPackage appFacingPackage = - createFakePlugin('plugin1', pluginGroup); - final RepositoryPackage platformInterfacePackage = - createFakePlugin('plugin1_platform_interface', pluginGroup); - final RepositoryPackage implementationPackage = - createFakePlugin('plugin1_web', pluginGroup); - - await runCapturingPrint( - runner, ['sample', '--base-sha=main', '--packages=plugin1']); - - expect( - command.plugins, - unorderedEquals([ - appFacingPackage.path, - platformInterfacePackage.path, - implementationPackage.path - ])); - }); - - test( - 'specifying the app-facing package of a federated plugin using its ' - 'fully qualified name should include only that package', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1/plugin1.dart -'''), - ]; - final Directory pluginGroup = packagesDir.childDirectory('plugin1'); - final RepositoryPackage appFacingPackage = - createFakePlugin('plugin1', pluginGroup); - createFakePlugin('plugin1_platform_interface', pluginGroup); - createFakePlugin('plugin1_web', pluginGroup); - - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--packages=plugin1/plugin1']); - - expect(command.plugins, unorderedEquals([appFacingPackage.path])); - }); - - test( - 'specifying a package of a federated plugin by its name should ' - 'include only that package', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1/plugin1.dart -'''), - ]; - final Directory pluginGroup = packagesDir.childDirectory('plugin1'); - - createFakePlugin('plugin1', pluginGroup); - final RepositoryPackage platformInterfacePackage = - createFakePlugin('plugin1_platform_interface', pluginGroup); - createFakePlugin('plugin1_web', pluginGroup); - - await runCapturingPrint(runner, [ - 'sample', - '--base-sha=main', - '--packages=plugin1_platform_interface' - ]); - - expect(command.plugins, - unorderedEquals([platformInterfacePackage.path])); - }); - - test('returns subpackages after the enclosing package', () async { - final SamplePackageCommand localCommand = SamplePackageCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: MockGitDir(), - includeSubpackages: true, - ); - final CommandRunner localRunner = - CommandRunner('common_command', 'subpackage testing'); - localRunner.addCommand(localCommand); - - final RepositoryPackage package = - createFakePackage('apackage', packagesDir); - - await runCapturingPrint(localRunner, ['sample']); - expect( - localCommand.plugins, - containsAllInOrder([ - package.path, - getExampleDir(package).path, - ])); - }); - - group('conflicting package selection', () { - test('does not allow --packages with --run-on-changed-packages', - () async { - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'sample', - '--run-on-changed-packages', - '--packages=plugin1', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Only one of --packages, --run-on-changed-packages, or ' - '--packages-for-branch can be provided.') - ])); - }); - - test('does not allow --packages with --packages-for-branch', () async { - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'sample', - '--packages-for-branch', - '--packages=plugin1', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Only one of --packages, --run-on-changed-packages, or ' - '--packages-for-branch can be provided.') - ])); - }); - - test( - 'does not allow --run-on-changed-packages with --packages-for-branch', - () async { - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'sample', - '--packages-for-branch', - '--packages=plugin1', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Only one of --packages, --run-on-changed-packages, or ' - '--packages-for-branch can be provided.') - ])); - }); - }); - - group('test run-on-changed-packages', () { - test('all plugins should be tested if there are no changes.', () async { - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - }); - - test( - 'all plugins should be tested if there are no plugin related changes.', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'AUTHORS'), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - }); - - test('all plugins should be tested if .cirrus.yml changes.', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -.cirrus.yml -packages/plugin1/CHANGELOG -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - expect( - output, - containsAllInOrder([ - contains('Running for all packages, since a file has changed ' - 'that could affect the entire repository.') - ])); - }); - - test('all plugins should be tested if .ci.yaml changes', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -.ci.yaml -packages/plugin1/CHANGELOG -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - expect( - output, - containsAllInOrder([ - contains('Running for all packages, since a file has changed ' - 'that could affect the entire repository.') - ])); - }); - - test('all plugins should be tested if anything in .ci/ changes', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -.ci/Dockerfile -packages/plugin1/CHANGELOG -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - expect( - output, - containsAllInOrder([ - contains('Running for all packages, since a file has changed ' - 'that could affect the entire repository.') - ])); - }); - - test('all plugins should be tested if anything in script/ changes.', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -script/tool_runner.sh -packages/plugin1/CHANGELOG -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - expect( - output, - containsAllInOrder([ - contains('Running for all packages, since a file has changed ' - 'that could affect the entire repository.') - ])); - }); - - test('all plugins should be tested if the root analysis options change.', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -analysis_options.yaml -packages/plugin1/CHANGELOG -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - expect( - output, - containsAllInOrder([ - contains('Running for all packages, since a file has changed ' - 'that could affect the entire repository.') - ])); - }); - - test('all plugins should be tested if formatting options change.', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -.clang-format -packages/plugin1/CHANGELOG -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - expect( - output, - containsAllInOrder([ - contains('Running for all packages, since a file has changed ' - 'that could affect the entire repository.') - ])); - }); - - test('Only changed plugin should be tested.', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'packages/plugin1/plugin1.dart'), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - final List output = await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect( - output, - containsAllInOrder([ - contains( - 'Running for all packages that have diffs relative to "main"'), - ])); - - expect(command.plugins, unorderedEquals([plugin1.path])); - }); - - test('multiple files in one plugin should also test the plugin', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1.dart -packages/plugin1/ios/plugin1.m -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, unorderedEquals([plugin1.path])); - }); - - test('multiple plugins changed should test all the changed plugins', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1.dart -packages/plugin2/ios/plugin2.m -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir); - createFakePlugin('plugin3', packagesDir); - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, - unorderedEquals([plugin1.path, plugin2.path])); - }); - - test( - 'multiple plugins inside the same plugin group changed should output the plugin group name', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1/plugin1.dart -packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart -packages/plugin1/plugin1_web/plugin1_web.dart -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); - createFakePlugin('plugin2', packagesDir); - createFakePlugin('plugin3', packagesDir); - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, unorderedEquals([plugin1.path])); - }); - - test( - 'changing one plugin in a federated group should only include that plugin', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1/plugin1.dart -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); - createFakePlugin('plugin1_platform_interface', - packagesDir.childDirectory('plugin1')); - createFakePlugin('plugin1_web', packagesDir.childDirectory('plugin1')); - await runCapturingPrint(runner, - ['sample', '--base-sha=main', '--run-on-changed-packages']); - - expect(command.plugins, unorderedEquals([plugin1.path])); - }); - - test('--exclude flag works with --run-on-changed-packages', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin1/plugin1.dart -packages/plugin2/ios/plugin2.m -packages/plugin3/plugin3.dart -'''), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); - createFakePlugin('plugin2', packagesDir); - createFakePlugin('plugin3', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--exclude=plugin2,plugin3', - '--base-sha=main', - '--run-on-changed-packages' - ]); - - expect(command.plugins, unorderedEquals([plugin1.path])); - }); - }); - - group('test run-on-dirty-packages', () { - test('no packages should be tested if there are no changes.', () async { - createFakePackage('a_package', packagesDir); - await runCapturingPrint( - runner, ['sample', '--run-on-dirty-packages']); - - expect(command.plugins, unorderedEquals([])); - }); - - test( - 'no packages should be tested if there are no plugin related changes.', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'AUTHORS'), - ]; - createFakePackage('a_package', packagesDir); - await runCapturingPrint( - runner, ['sample', '--run-on-dirty-packages']); - - expect(command.plugins, unorderedEquals([])); - }); - - test('no packages should be tested even if special repo files change.', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -.cirrus.yml -.ci.yaml -.ci/Dockerfile -.clang-format -analysis_options.yaml -script/tool_runner.sh -'''), - ]; - createFakePackage('a_package', packagesDir); - await runCapturingPrint( - runner, ['sample', '--run-on-dirty-packages']); - - expect(command.plugins, unorderedEquals([])); - }); - - test('Only changed packages should be tested.', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'packages/a_package/lib/a_package.dart'), - ]; - final RepositoryPackage packageA = - createFakePackage('a_package', packagesDir); - createFakePlugin('b_package', packagesDir); - final List output = await runCapturingPrint( - runner, ['sample', '--run-on-dirty-packages']); - - expect( - output, - containsAllInOrder([ - contains( - 'Running for all packages that have uncommitted changes'), - ])); - - expect(command.plugins, unorderedEquals([packageA.path])); - }); - - test('multiple packages changed should test all the changed packages', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/a_package/lib/a_package.dart -packages/b_package/lib/src/foo.dart -'''), - ]; - final RepositoryPackage packageA = - createFakePackage('a_package', packagesDir); - final RepositoryPackage packageB = - createFakePackage('b_package', packagesDir); - createFakePackage('c_package', packagesDir); - await runCapturingPrint( - runner, ['sample', '--run-on-dirty-packages']); - - expect(command.plugins, - unorderedEquals([packageA.path, packageB.path])); - }); - - test('honors --exclude flag', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/a_package/lib/a_package.dart -packages/b_package/lib/src/foo.dart -'''), - ]; - final RepositoryPackage packageA = - createFakePackage('a_package', packagesDir); - createFakePackage('b_package', packagesDir); - createFakePackage('c_package', packagesDir); - await runCapturingPrint(runner, [ - 'sample', - '--exclude=b_package', - '--run-on-dirty-packages' - ]); - - expect(command.plugins, unorderedEquals([packageA.path])); - }); - }); - }); - - group('--packages-for-branch', () { - test('only tests changed packages relative to the merge base on a branch', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'packages/plugin1/plugin1.dart'), - ]; - processRunner.mockProcessesForExecutable['git-rev-parse'] = [ - MockProcess(stdout: 'a-branch'), - ]; - processRunner.mockProcessesForExecutable['git-merge-base'] = [ - MockProcess(stdout: 'abc123'), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - - final List output = await runCapturingPrint( - runner, ['sample', '--packages-for-branch']); - - expect(command.plugins, unorderedEquals([plugin1.path])); - expect( - output, - containsAllInOrder([ - contains( - 'Running for all packages that have diffs relative to "abc123"'), - ])); - // Ensure that it's diffing against the merge-base. - expect( - processRunner.recordedCalls, - contains( - const ProcessCall( - 'git-diff', ['--name-only', 'abc123', 'HEAD'], null), - )); - }); - - test('only tests changed packages relative to the previous commit on main', - () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'packages/plugin1/plugin1.dart'), - ]; - processRunner.mockProcessesForExecutable['git-rev-parse'] = [ - MockProcess(stdout: 'main'), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - - final List output = await runCapturingPrint( - runner, ['sample', '--packages-for-branch']); - - expect(command.plugins, unorderedEquals([plugin1.path])); - expect( - output, - containsAllInOrder([ - contains('--packages-for-branch: running on default branch; ' - 'using parent commit as the diff base'), - contains( - 'Running for all packages that have diffs relative to "HEAD~"'), - ])); - // Ensure that it's diffing against the prior commit. - expect( - processRunner.recordedCalls, - contains( - const ProcessCall( - 'git-diff', ['--name-only', 'HEAD~', 'HEAD'], null), - )); - }); - - test('tests all packages on master', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'packages/plugin1/plugin1.dart'), - ]; - processRunner.mockProcessesForExecutable['git-rev-parse'] = [ - MockProcess(stdout: 'master'), - ]; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir); - - final List output = await runCapturingPrint( - runner, ['sample', '--packages-for-branch']); - - expect(command.plugins, unorderedEquals([plugin1.path])); - expect( - output, - containsAllInOrder([ - contains('--packages-for-branch: running on default branch; ' - 'using parent commit as the diff base'), - contains( - 'Running for all packages that have diffs relative to "HEAD~"'), - ])); - // Ensure that it's diffing against the prior commit. - expect( - processRunner.recordedCalls, - contains( - const ProcessCall( - 'git-diff', ['--name-only', 'HEAD~', 'HEAD'], null), - )); - }); - - test('throws if getting the branch fails', () async { - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: 'packages/plugin1/plugin1.dart'), - ]; - processRunner.mockProcessesForExecutable['git-rev-parse'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['sample', '--packages-for-branch'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unabled to determine branch'), - ])); - }); - }); - - group('sharding', () { - test('distributes evenly when evenly divisible', () async { - final List> expectedShards = - >[ - [ - createFakePackage('package1', packagesDir), - createFakePackage('package2', packagesDir), - createFakePackage('package3', packagesDir), - ], - [ - createFakePackage('package4', packagesDir), - createFakePackage('package5', packagesDir), - createFakePackage('package6', packagesDir), - ], - [ - createFakePackage('package7', packagesDir), - createFakePackage('package8', packagesDir), - createFakePackage('package9', packagesDir), - ], - ]; - - for (int i = 0; i < expectedShards.length; ++i) { - final SamplePackageCommand localCommand = SamplePackageCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: MockGitDir(), - ); - final CommandRunner localRunner = - CommandRunner('common_command', 'Shard testing'); - localRunner.addCommand(localCommand); - - await runCapturingPrint(localRunner, [ - 'sample', - '--shardIndex=$i', - '--shardCount=3', - ]); - expect( - localCommand.plugins, - unorderedEquals(expectedShards[i] - .map((RepositoryPackage package) => package.path) - .toList())); - } - }); - - test('distributes as evenly as possible when not evenly divisible', - () async { - final List> expectedShards = - >[ - [ - createFakePackage('package1', packagesDir), - createFakePackage('package2', packagesDir), - createFakePackage('package3', packagesDir), - ], - [ - createFakePackage('package4', packagesDir), - createFakePackage('package5', packagesDir), - createFakePackage('package6', packagesDir), - ], - [ - createFakePackage('package7', packagesDir), - createFakePackage('package8', packagesDir), - ], - ]; - - for (int i = 0; i < expectedShards.length; ++i) { - final SamplePackageCommand localCommand = SamplePackageCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: MockGitDir(), - ); - final CommandRunner localRunner = - CommandRunner('common_command', 'Shard testing'); - localRunner.addCommand(localCommand); - - await runCapturingPrint(localRunner, [ - 'sample', - '--shardIndex=$i', - '--shardCount=3', - ]); - expect( - localCommand.plugins, - unorderedEquals(expectedShards[i] - .map((RepositoryPackage package) => package.path) - .toList())); - } - }); - - // In CI (which is the use case for sharding) we often want to run muliple - // commands on the same set of packages, but the exclusion lists for those - // commands may be different. In those cases we still want all the commands - // to operate on a consistent set of plugins. - // - // E.g., some commands require running build-examples in a previous step; - // excluding some plugins from the later step shouldn't change what's tested - // in each shard, as it may no longer align with what was built. - test('counts excluded plugins when sharding', () async { - final List> expectedShards = - >[ - [ - createFakePackage('package1', packagesDir), - createFakePackage('package2', packagesDir), - createFakePackage('package3', packagesDir), - ], - [ - createFakePackage('package4', packagesDir), - createFakePackage('package5', packagesDir), - createFakePackage('package6', packagesDir), - ], - [ - createFakePackage('package7', packagesDir), - ], - ]; - // These would be in the last shard, but are excluded. - createFakePackage('package8', packagesDir); - createFakePackage('package9', packagesDir); - - for (int i = 0; i < expectedShards.length; ++i) { - final SamplePackageCommand localCommand = SamplePackageCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: MockGitDir(), - ); - final CommandRunner localRunner = - CommandRunner('common_command', 'Shard testing'); - localRunner.addCommand(localCommand); - - await runCapturingPrint(localRunner, [ - 'sample', - '--shardIndex=$i', - '--shardCount=3', - '--exclude=package8,package9', - ]); - expect( - localCommand.plugins, - unorderedEquals(expectedShards[i] - .map((RepositoryPackage package) => package.path) - .toList())); - } - }); - }); -} - -class SamplePackageCommand extends PackageCommand { - SamplePackageCommand( - Directory packagesDir, { - ProcessRunner processRunner = const ProcessRunner(), - Platform platform = const LocalPlatform(), - GitDir? gitDir, - this.includeSubpackages = false, - }) : super(packagesDir, - processRunner: processRunner, platform: platform, gitDir: gitDir); - - final List plugins = []; - - final bool includeSubpackages; - - @override - final String name = 'sample'; - - @override - final String description = 'sample command'; - - @override - Future run() async { - final Stream packages = includeSubpackages - ? getTargetPackagesAndSubpackages() - : getTargetPackages(); - await for (final PackageEnumerationEntry entry in packages) { - plugins.add(entry.package.path); - } - } -} diff --git a/script/tool/test/common/package_command_test.mocks.dart b/script/tool/test/common/package_command_test.mocks.dart deleted file mode 100644 index 79c5d4df1a8c..000000000000 --- a/script/tool/test/common/package_command_test.mocks.dart +++ /dev/null @@ -1,286 +0,0 @@ -// Mocks generated by Mockito 5.3.2 from annotations -// in flutter_plugin_tools/test/common/package_command_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i6; -import 'dart:io' as _i4; - -import 'package:git/src/branch_reference.dart' as _i3; -import 'package:git/src/commit.dart' as _i2; -import 'package:git/src/commit_reference.dart' as _i8; -import 'package:git/src/git_dir.dart' as _i5; -import 'package:git/src/tag.dart' as _i7; -import 'package:git/src/tree_entry.dart' as _i9; -import 'package:mockito/mockito.dart' as _i1; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class - -class _FakeCommit_0 extends _i1.SmartFake implements _i2.Commit { - _FakeCommit_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeBranchReference_1 extends _i1.SmartFake - implements _i3.BranchReference { - _FakeBranchReference_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeProcessResult_2 extends _i1.SmartFake implements _i4.ProcessResult { - _FakeProcessResult_2( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -/// A class which mocks [GitDir]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockGitDir extends _i1.Mock implements _i5.GitDir { - MockGitDir() { - _i1.throwOnMissingStub(this); - } - - @override - String get path => (super.noSuchMethod( - Invocation.getter(#path), - returnValue: '', - ) as String); - @override - _i6.Future commitCount([String? branchName = r'HEAD']) => - (super.noSuchMethod( - Invocation.method( - #commitCount, - [branchName], - ), - returnValue: _i6.Future.value(0), - ) as _i6.Future); - @override - _i6.Future<_i2.Commit> commitFromRevision(String? revision) => - (super.noSuchMethod( - Invocation.method( - #commitFromRevision, - [revision], - ), - returnValue: _i6.Future<_i2.Commit>.value(_FakeCommit_0( - this, - Invocation.method( - #commitFromRevision, - [revision], - ), - )), - ) as _i6.Future<_i2.Commit>); - @override - _i6.Future> commits([String? branchName = r'HEAD']) => - (super.noSuchMethod( - Invocation.method( - #commits, - [branchName], - ), - returnValue: - _i6.Future>.value({}), - ) as _i6.Future>); - @override - _i6.Future<_i3.BranchReference?> branchReference(String? branchName) => - (super.noSuchMethod( - Invocation.method( - #branchReference, - [branchName], - ), - returnValue: _i6.Future<_i3.BranchReference?>.value(), - ) as _i6.Future<_i3.BranchReference?>); - @override - _i6.Future> branches() => (super.noSuchMethod( - Invocation.method( - #branches, - [], - ), - returnValue: _i6.Future>.value( - <_i3.BranchReference>[]), - ) as _i6.Future>); - @override - _i6.Stream<_i7.Tag> tags() => (super.noSuchMethod( - Invocation.method( - #tags, - [], - ), - returnValue: _i6.Stream<_i7.Tag>.empty(), - ) as _i6.Stream<_i7.Tag>); - @override - _i6.Future> showRef({ - bool? heads = false, - bool? tags = false, - }) => - (super.noSuchMethod( - Invocation.method( - #showRef, - [], - { - #heads: heads, - #tags: tags, - }, - ), - returnValue: _i6.Future>.value( - <_i8.CommitReference>[]), - ) as _i6.Future>); - @override - _i6.Future<_i3.BranchReference> currentBranch() => (super.noSuchMethod( - Invocation.method( - #currentBranch, - [], - ), - returnValue: - _i6.Future<_i3.BranchReference>.value(_FakeBranchReference_1( - this, - Invocation.method( - #currentBranch, - [], - ), - )), - ) as _i6.Future<_i3.BranchReference>); - @override - _i6.Future> lsTree( - String? treeish, { - bool? subTreesOnly = false, - String? path, - }) => - (super.noSuchMethod( - Invocation.method( - #lsTree, - [treeish], - { - #subTreesOnly: subTreesOnly, - #path: path, - }, - ), - returnValue: _i6.Future>.value(<_i9.TreeEntry>[]), - ) as _i6.Future>); - @override - _i6.Future createOrUpdateBranch( - String? branchName, - String? treeSha, - String? commitMessage, - ) => - (super.noSuchMethod( - Invocation.method( - #createOrUpdateBranch, - [ - branchName, - treeSha, - commitMessage, - ], - ), - returnValue: _i6.Future.value(), - ) as _i6.Future); - @override - _i6.Future commitTree( - String? treeSha, - String? commitMessage, { - List? parentCommitShas, - }) => - (super.noSuchMethod( - Invocation.method( - #commitTree, - [ - treeSha, - commitMessage, - ], - {#parentCommitShas: parentCommitShas}, - ), - returnValue: _i6.Future.value(''), - ) as _i6.Future); - @override - _i6.Future> writeObjects(List? paths) => - (super.noSuchMethod( - Invocation.method( - #writeObjects, - [paths], - ), - returnValue: _i6.Future>.value({}), - ) as _i6.Future>); - @override - _i6.Future<_i4.ProcessResult> runCommand( - Iterable? args, { - bool? throwOnError = true, - }) => - (super.noSuchMethod( - Invocation.method( - #runCommand, - [args], - {#throwOnError: throwOnError}, - ), - returnValue: _i6.Future<_i4.ProcessResult>.value(_FakeProcessResult_2( - this, - Invocation.method( - #runCommand, - [args], - {#throwOnError: throwOnError}, - ), - )), - ) as _i6.Future<_i4.ProcessResult>); - @override - _i6.Future isWorkingTreeClean() => (super.noSuchMethod( - Invocation.method( - #isWorkingTreeClean, - [], - ), - returnValue: _i6.Future.value(false), - ) as _i6.Future); - @override - _i6.Future<_i2.Commit?> updateBranch( - String? branchName, - _i6.Future Function(_i4.Directory)? populater, - String? commitMessage, - ) => - (super.noSuchMethod( - Invocation.method( - #updateBranch, - [ - branchName, - populater, - commitMessage, - ], - ), - returnValue: _i6.Future<_i2.Commit?>.value(), - ) as _i6.Future<_i2.Commit?>); - @override - _i6.Future<_i2.Commit?> updateBranchWithDirectoryContents( - String? branchName, - String? sourceDirectoryPath, - String? commitMessage, - ) => - (super.noSuchMethod( - Invocation.method( - #updateBranchWithDirectoryContents, - [ - branchName, - sourceDirectoryPath, - commitMessage, - ], - ), - returnValue: _i6.Future<_i2.Commit?>.value(), - ) as _i6.Future<_i2.Commit?>); -} diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart deleted file mode 100644 index 34f346c62fe7..000000000000 --- a/script/tool/test/common/package_looping_command_test.dart +++ /dev/null @@ -1,949 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/package_looping_command.dart'; -import 'package:flutter_plugin_tools/src/common/process_runner.dart'; -import 'package:git/git.dart'; -import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import '../mocks.dart'; -import '../util.dart'; -import 'package_command_test.mocks.dart'; - -// Constants for colorized output start and end. -const String _startElapsedTimeColor = '\x1B[90m'; -const String _startErrorColor = '\x1B[31m'; -const String _startHeadingColor = '\x1B[36m'; -const String _startSkipColor = '\x1B[90m'; -const String _startSkipWithWarningColor = '\x1B[93m'; -const String _startSuccessColor = '\x1B[32m'; -const String _startWarningColor = '\x1B[33m'; -const String _endColor = '\x1B[0m'; - -// The filename within a package containing warnings to log during runForPackage. -enum _ResultFileType { - /// A file containing errors to return. - errors, - - /// A file containing warnings that should be logged. - warns, - - /// A file indicating that the package should be skipped, and why. - skips, - - /// A file indicating that the package should throw. - throws, -} - -// The filename within a package containing errors to return from runForPackage. -const String _errorFile = 'errors'; -// The filename within a package indicating that it should be skipped. -const String _skipFile = 'skip'; -// The filename within a package containing warnings to log during runForPackage. -const String _warningFile = 'warnings'; -// The filename within a package indicating that it should throw. -const String _throwFile = 'throw'; - -/// Writes a file to [package] to control the behavior of -/// [TestPackageLoopingCommand] for that package. -void _addResultFile(RepositoryPackage package, _ResultFileType type, - {String? contents}) { - final File file = package.directory.childFile(_filenameForType(type)); - file.createSync(); - if (contents != null) { - file.writeAsStringSync(contents); - } -} - -String _filenameForType(_ResultFileType type) { - switch (type) { - case _ResultFileType.errors: - return _errorFile; - case _ResultFileType.warns: - return _warningFile; - case _ResultFileType.skips: - return _skipFile; - case _ResultFileType.throws: - return _throwFile; - } -} - -void main() { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late Directory thirdPartyPackagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - thirdPartyPackagesDir = packagesDir.parent - .childDirectory('third_party') - .childDirectory('packages'); - }); - - /// Creates a TestPackageLoopingCommand instance that uses [gitDiffResponse] - /// for git diffs, and logs output to [printOutput]. - TestPackageLoopingCommand createTestCommand({ - String gitDiffResponse = '', - bool hasLongOutput = true, - PackageLoopingType packageLoopingType = PackageLoopingType.topLevelOnly, - bool failsDuringInit = false, - bool warnsDuringInit = false, - bool warnsDuringCleanup = false, - bool captureOutput = false, - String? customFailureListHeader, - String? customFailureListFooter, - }) { - // Set up the git diff response. - final MockGitDir gitDir = MockGitDir(); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - final MockProcessResult mockProcessResult = MockProcessResult(); - if (arguments[0] == 'diff') { - when(mockProcessResult.stdout as String?) - .thenReturn(gitDiffResponse); - } - return Future.value(mockProcessResult); - }); - - return TestPackageLoopingCommand( - packagesDir, - platform: mockPlatform, - hasLongOutput: hasLongOutput, - packageLoopingType: packageLoopingType, - failsDuringInit: failsDuringInit, - warnsDuringInit: warnsDuringInit, - warnsDuringCleanup: warnsDuringCleanup, - customFailureListHeader: customFailureListHeader, - customFailureListFooter: customFailureListFooter, - captureOutput: captureOutput, - gitDir: gitDir, - ); - } - - /// Runs [command] with the given [arguments], and returns its output. - Future> runCommand( - TestPackageLoopingCommand command, { - List arguments = const [], - void Function(Error error)? errorHandler, - }) async { - late CommandRunner runner; - runner = CommandRunner('test_package_looping_command', - 'Test for base package looping functionality'); - runner.addCommand(command); - return runCapturingPrint( - runner, - [command.name, ...arguments], - errorHandler: errorHandler, - ); - } - - group('tool exit', () { - test('is handled during initializeRun', () async { - final TestPackageLoopingCommand command = - createTestCommand(failsDuringInit: true); - - expect(() => runCommand(command), throwsA(isA())); - }); - - test('does not stop looping on error', () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage failingPackage = - createFakePlugin('package_b', packagesDir); - createFakePackage('package_c', packagesDir); - _addResultFile(failingPackage, _ResultFileType.errors); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - Error? commandError; - final List output = - await runCommand(command, errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startHeadingColor}Running for package_b...$_endColor', - '${_startHeadingColor}Running for package_c...$_endColor', - ])); - }); - - test('does not stop looping on exceptions', () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage failingPackage = - createFakePlugin('package_b', packagesDir); - createFakePackage('package_c', packagesDir); - _addResultFile(failingPackage, _ResultFileType.throws); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - Error? commandError; - final List output = - await runCommand(command, errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startHeadingColor}Running for package_b...$_endColor', - '${_startHeadingColor}Running for package_c...$_endColor', - ])); - }); - }); - - group('package iteration', () { - test('includes plugins and packages', () async { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir); - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand(); - await runCommand(command); - - expect(command.checkedPackages, - unorderedEquals([plugin.path, package.path])); - }); - - test('includes third_party/packages', () async { - final RepositoryPackage package1 = - createFakePackage('a_package', packagesDir); - final RepositoryPackage package2 = - createFakePackage('another_package', thirdPartyPackagesDir); - - final TestPackageLoopingCommand command = createTestCommand(); - await runCommand(command); - - expect(command.checkedPackages, - unorderedEquals([package1.path, package2.path])); - }); - - test('includes all subpackages when requested', () async { - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, - examples: ['example1', 'example2']); - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - final RepositoryPackage subPackage = createFakePackage( - 'sub_package', package.directory, - examples: []); - - final TestPackageLoopingCommand command = createTestCommand( - packageLoopingType: PackageLoopingType.includeAllSubpackages); - await runCommand(command); - - expect( - command.checkedPackages, - unorderedEquals([ - plugin.path, - getExampleDir(plugin).childDirectory('example1').path, - getExampleDir(plugin).childDirectory('example2').path, - package.path, - getExampleDir(package).path, - subPackage.path, - ])); - }); - - test('includes examples when requested', () async { - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, - examples: ['example1', 'example2']); - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - final RepositoryPackage subPackage = - createFakePackage('sub_package', package.directory); - - final TestPackageLoopingCommand command = createTestCommand( - packageLoopingType: PackageLoopingType.includeExamples); - await runCommand(command); - - expect( - command.checkedPackages, - unorderedEquals([ - plugin.path, - getExampleDir(plugin).childDirectory('example1').path, - getExampleDir(plugin).childDirectory('example2').path, - package.path, - getExampleDir(package).path, - ])); - expect(command.checkedPackages, isNot(contains(subPackage.path))); - }); - - test('excludes subpackages when main package is excluded', () async { - final RepositoryPackage excluded = createFakePlugin( - 'a_plugin', packagesDir, - examples: ['example1', 'example2']); - final RepositoryPackage included = - createFakePackage('a_package', packagesDir); - final RepositoryPackage subpackage = - createFakePackage('sub_package', excluded.directory); - - final TestPackageLoopingCommand command = createTestCommand( - packageLoopingType: PackageLoopingType.includeAllSubpackages); - await runCommand(command, arguments: ['--exclude=a_plugin']); - - final Iterable examples = excluded.getExamples(); - - expect( - command.checkedPackages, - unorderedEquals([ - included.path, - getExampleDir(included).path, - ])); - expect(command.checkedPackages, isNot(contains(excluded.path))); - expect(examples.length, 2); - for (final RepositoryPackage example in examples) { - expect(command.checkedPackages, isNot(contains(example.path))); - } - expect(command.checkedPackages, isNot(contains(subpackage.path))); - }); - - test('excludes examples when main package is excluded', () async { - final RepositoryPackage excluded = createFakePlugin( - 'a_plugin', packagesDir, - examples: ['example1', 'example2']); - final RepositoryPackage included = - createFakePackage('a_package', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand( - packageLoopingType: PackageLoopingType.includeExamples); - await runCommand(command, arguments: ['--exclude=a_plugin']); - - final Iterable examples = excluded.getExamples(); - - expect( - command.checkedPackages, - unorderedEquals([ - included.path, - getExampleDir(included).path, - ])); - expect(command.checkedPackages, isNot(contains(excluded.path))); - expect(examples.length, 2); - for (final RepositoryPackage example in examples) { - expect(command.checkedPackages, isNot(contains(example.path))); - } - }); - - test('skips unsupported Flutter versions when requested', () async { - final RepositoryPackage excluded = createFakePlugin( - 'a_plugin', packagesDir, - flutterConstraint: '>=2.10.0'); - final RepositoryPackage included = - createFakePackage('a_package', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand( - packageLoopingType: PackageLoopingType.includeAllSubpackages, - hasLongOutput: false); - final List output = await runCommand(command, arguments: [ - '--skip-if-not-supporting-flutter-version=2.5.0' - ]); - - expect( - command.checkedPackages, - unorderedEquals([ - included.path, - getExampleDir(included).path, - ])); - expect(command.checkedPackages, isNot(contains(excluded.path))); - - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for a_package...$_endColor', - '${_startHeadingColor}Running for a_plugin...$_endColor', - '$_startSkipColor SKIPPING: Does not support Flutter 2.5.0$_endColor', - ])); - }); - - test('skips unsupported Dart versions when requested', () async { - final RepositoryPackage excluded = createFakePackage( - 'excluded_package', packagesDir, - dartConstraint: '>=2.17.0 <3.0.0'); - final RepositoryPackage included = - createFakePackage('a_package', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand( - packageLoopingType: PackageLoopingType.includeAllSubpackages, - hasLongOutput: false); - final List output = await runCommand(command, - arguments: ['--skip-if-not-supporting-dart-version=2.14.0']); - - expect( - command.checkedPackages, - unorderedEquals([ - included.path, - getExampleDir(included).path, - ])); - expect(command.checkedPackages, isNot(contains(excluded.path))); - - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for a_package...$_endColor', - '${_startHeadingColor}Running for excluded_package...$_endColor', - '$_startSkipColor SKIPPING: Does not support Dart 2.14.0$_endColor', - ])); - }); - }); - - group('output', () { - test('has the expected package headers for long-form output', () async { - createFakePlugin('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand(); - final List output = await runCommand(command); - - const String separator = - '============================================================'; - expect( - output, - containsAllInOrder([ - '$_startHeadingColor\n$separator\n|| Running for package_a\n$separator\n$_endColor', - '$_startHeadingColor\n$separator\n|| Running for package_b\n$separator\n$_endColor', - ])); - }); - - test('has the expected package headers for short-form output', () async { - createFakePlugin('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startHeadingColor}Running for package_b...$_endColor', - ])); - }); - - test('prints timing info in long-form output when requested', () async { - createFakePlugin('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand(); - final List output = - await runCommand(command, arguments: ['--log-timing']); - - const String separator = - '============================================================'; - expect( - output, - containsAllInOrder([ - '$_startHeadingColor\n$separator\n|| Running for package_a [@0:00]\n$separator\n$_endColor', - '$_startElapsedTimeColor\n[package_a completed in 0m 0s]$_endColor', - '$_startHeadingColor\n$separator\n|| Running for package_b [@0:00]\n$separator\n$_endColor', - '$_startElapsedTimeColor\n[package_b completed in 0m 0s]$_endColor', - ])); - }); - - test('prints timing info in short-form output when requested', () async { - createFakePlugin('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = - await runCommand(command, arguments: ['--log-timing']); - - expect( - output, - containsAllInOrder([ - '$_startHeadingColor[0:00] Running for package_a...$_endColor', - '$_startHeadingColor[0:00] Running for package_b...$_endColor', - ])); - // Short-form output should not include elapsed time. - expect(output, isNot(contains('[package_a completed in 0m 0s]'))); - }); - - test('shows the success message when nothing fails', () async { - createFakePackage('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '\n', - '${_startSuccessColor}No issues found!$_endColor', - ])); - }); - - test('shows failure summaries when something fails without extra details', - () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage failingPackage1 = - createFakePlugin('package_b', packagesDir); - createFakePackage('package_c', packagesDir); - final RepositoryPackage failingPackage2 = - createFakePlugin('package_d', packagesDir); - _addResultFile(failingPackage1, _ResultFileType.errors); - _addResultFile(failingPackage2, _ResultFileType.errors); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - Error? commandError; - final List output = - await runCommand(command, errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - '\n', - '${_startErrorColor}The following packages had errors:$_endColor', - '$_startErrorColor package_b$_endColor', - '$_startErrorColor package_d$_endColor', - '${_startErrorColor}See above for full details.$_endColor', - ])); - }); - - test('uses custom summary header and footer if provided', () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage failingPackage1 = - createFakePlugin('package_b', packagesDir); - createFakePackage('package_c', packagesDir); - final RepositoryPackage failingPackage2 = - createFakePlugin('package_d', packagesDir); - _addResultFile(failingPackage1, _ResultFileType.errors); - _addResultFile(failingPackage2, _ResultFileType.errors); - - final TestPackageLoopingCommand command = createTestCommand( - hasLongOutput: false, - customFailureListHeader: 'This is a custom header', - customFailureListFooter: 'And a custom footer!'); - Error? commandError; - final List output = - await runCommand(command, errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - '\n', - '${_startErrorColor}This is a custom header$_endColor', - '$_startErrorColor package_b$_endColor', - '$_startErrorColor package_d$_endColor', - '${_startErrorColor}And a custom footer!$_endColor', - ])); - }); - - test('shows failure summaries when something fails with extra details', - () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage failingPackage1 = - createFakePlugin('package_b', packagesDir); - createFakePackage('package_c', packagesDir); - final RepositoryPackage failingPackage2 = - createFakePlugin('package_d', packagesDir); - _addResultFile(failingPackage1, _ResultFileType.errors, - contents: 'just one detail'); - _addResultFile(failingPackage2, _ResultFileType.errors, - contents: 'first detail\nsecond detail'); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - Error? commandError; - final List output = - await runCommand(command, errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - '\n', - '${_startErrorColor}The following packages had errors:$_endColor', - '$_startErrorColor package_b:\n just one detail$_endColor', - '$_startErrorColor package_d:\n first detail\n second detail$_endColor', - '${_startErrorColor}See above for full details.$_endColor', - ])); - }); - - test('is captured, not printed, when requested', () async { - createFakePlugin('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(captureOutput: true); - final List output = await runCommand(command); - - expect(output, isEmpty); - - // None of the output should be colorized when captured. - const String separator = - '============================================================'; - expect( - command.capturedOutput, - containsAllInOrder([ - '\n$separator\n|| Running for package_a\n$separator\n', - '\n$separator\n|| Running for package_b\n$separator\n', - 'No issues found!', - ])); - }); - - test('logs skips', () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage skipPackage = - createFakePackage('package_b', packagesDir); - _addResultFile(skipPackage, _ResultFileType.skips, - contents: 'For a reason'); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startHeadingColor}Running for package_b...$_endColor', - '$_startSkipColor SKIPPING: For a reason$_endColor', - ])); - }); - - test('logs exclusions', () async { - createFakePackage('package_a', packagesDir); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = - await runCommand(command, arguments: ['--exclude=package_b']); - - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startSkipColor}Not running for package_b; excluded$_endColor', - ])); - }); - - test('logs warnings', () async { - final RepositoryPackage warnPackage = - createFakePackage('package_a', packagesDir); - _addResultFile(warnPackage, _ResultFileType.warns, - contents: 'Warning 1\nWarning 2'); - createFakePackage('package_b', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startWarningColor}Warning 1$_endColor', - '${_startWarningColor}Warning 2$_endColor', - '${_startHeadingColor}Running for package_b...$_endColor', - ])); - }); - - test('logs unhandled exceptions as errors', () async { - createFakePackage('package_a', packagesDir); - final RepositoryPackage failingPackage = - createFakePlugin('package_b', packagesDir); - createFakePackage('package_c', packagesDir); - _addResultFile(failingPackage, _ResultFileType.throws); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - Error? commandError; - final List output = - await runCommand(command, errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - '${_startErrorColor}Exception: Uh-oh$_endColor', - '${_startErrorColor}The following packages had errors:$_endColor', - '$_startErrorColor package_b:\n Unhandled exception$_endColor', - ])); - }); - - test('prints run summary on success', () async { - final RepositoryPackage warnPackage1 = - createFakePackage('package_a', packagesDir); - _addResultFile(warnPackage1, _ResultFileType.warns, - contents: 'Warning 1\nWarning 2'); - - createFakePackage('package_b', packagesDir); - - final RepositoryPackage skipPackage = - createFakePackage('package_c', packagesDir); - _addResultFile(skipPackage, _ResultFileType.skips, - contents: 'For a reason'); - - final RepositoryPackage skipAndWarnPackage = - createFakePackage('package_d', packagesDir); - _addResultFile(skipAndWarnPackage, _ResultFileType.warns, - contents: 'Warning'); - _addResultFile(skipAndWarnPackage, _ResultFileType.skips, - contents: 'See warning'); - - final RepositoryPackage warnPackage2 = - createFakePackage('package_e', packagesDir); - _addResultFile(warnPackage2, _ResultFileType.warns, - contents: 'Warning 1\nWarning 2'); - - createFakePackage('package_f', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '------------------------------------------------------------', - 'Ran for 4 package(s) (2 with warnings)', - 'Skipped 2 package(s) (1 with warnings)', - '\n', - '${_startSuccessColor}No issues found!$_endColor', - ])); - // The long-form summary should not be printed for short-form commands. - expect(output, isNot(contains('Run summary:'))); - expect(output, isNot(contains(contains('package a - ran')))); - }); - - test('counts exclusions as skips in run summary', () async { - createFakePackage('package_a', packagesDir); - - final TestPackageLoopingCommand command = - createTestCommand(hasLongOutput: false); - final List output = - await runCommand(command, arguments: ['--exclude=package_a']); - - expect( - output, - containsAllInOrder([ - '------------------------------------------------------------', - 'Skipped 1 package(s)', - '\n', - '${_startSuccessColor}No issues found!$_endColor', - ])); - }); - - test('prints long-form run summary for long-output commands', () async { - final RepositoryPackage warnPackage1 = - createFakePackage('package_a', packagesDir); - _addResultFile(warnPackage1, _ResultFileType.warns, - contents: 'Warning 1\nWarning 2'); - - createFakePackage('package_b', packagesDir); - - final RepositoryPackage skipPackage = - createFakePackage('package_c', packagesDir); - _addResultFile(skipPackage, _ResultFileType.skips, - contents: 'For a reason'); - - final RepositoryPackage skipAndWarnPackage = - createFakePackage('package_d', packagesDir); - _addResultFile(skipAndWarnPackage, _ResultFileType.warns, - contents: 'Warning'); - _addResultFile(skipAndWarnPackage, _ResultFileType.skips, - contents: 'See warning'); - - final RepositoryPackage warnPackage2 = - createFakePackage('package_e', packagesDir); - _addResultFile(warnPackage2, _ResultFileType.warns, - contents: 'Warning 1\nWarning 2'); - - createFakePackage('package_f', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand(); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '------------------------------------------------------------', - 'Run overview:', - ' package_a - ${_startWarningColor}ran (with warning)$_endColor', - ' package_b - ${_startSuccessColor}ran$_endColor', - ' package_c - ${_startSkipColor}skipped$_endColor', - ' package_d - ${_startSkipWithWarningColor}skipped (with warning)$_endColor', - ' package_e - ${_startWarningColor}ran (with warning)$_endColor', - ' package_f - ${_startSuccessColor}ran$_endColor', - '', - 'Ran for 4 package(s) (2 with warnings)', - 'Skipped 2 package(s) (1 with warnings)', - '\n', - '${_startSuccessColor}No issues found!$_endColor', - ])); - }); - - test('prints exclusions as skips in long-form run summary', () async { - createFakePackage('package_a', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand(); - final List output = - await runCommand(command, arguments: ['--exclude=package_a']); - - expect( - output, - containsAllInOrder([ - ' package_a - ${_startSkipColor}excluded$_endColor', - '', - 'Skipped 1 package(s)', - '\n', - '${_startSuccessColor}No issues found!$_endColor', - ])); - }); - - test('handles warnings outside of runForPackage', () async { - createFakePackage('package_a', packagesDir); - - final TestPackageLoopingCommand command = createTestCommand( - hasLongOutput: false, - warnsDuringCleanup: true, - warnsDuringInit: true, - ); - final List output = await runCommand(command); - - expect( - output, - containsAllInOrder([ - '${_startWarningColor}Warning during initializeRun$_endColor', - '${_startHeadingColor}Running for package_a...$_endColor', - '${_startWarningColor}Warning during completeRun$_endColor', - '------------------------------------------------------------', - 'Ran for 1 package(s)', - '2 warnings not associated with a package', - '\n', - '${_startSuccessColor}No issues found!$_endColor', - ])); - }); - }); -} - -class TestPackageLoopingCommand extends PackageLoopingCommand { - TestPackageLoopingCommand( - Directory packagesDir, { - required Platform platform, - this.hasLongOutput = true, - this.packageLoopingType = PackageLoopingType.topLevelOnly, - this.customFailureListHeader, - this.customFailureListFooter, - this.failsDuringInit = false, - this.warnsDuringInit = false, - this.warnsDuringCleanup = false, - this.captureOutput = false, - ProcessRunner processRunner = const ProcessRunner(), - GitDir? gitDir, - }) : super(packagesDir, - processRunner: processRunner, platform: platform, gitDir: gitDir); - - final List checkedPackages = []; - final List capturedOutput = []; - - final String? customFailureListHeader; - final String? customFailureListFooter; - - final bool failsDuringInit; - final bool warnsDuringInit; - final bool warnsDuringCleanup; - - @override - bool hasLongOutput; - - @override - PackageLoopingType packageLoopingType; - - @override - String get failureListHeader => - customFailureListHeader ?? super.failureListHeader; - - @override - String get failureListFooter => - customFailureListFooter ?? super.failureListFooter; - - @override - bool captureOutput; - - @override - final String name = 'loop-test'; - - @override - final String description = 'sample package looping command'; - - @override - Future initializeRun() async { - if (warnsDuringInit) { - logWarning('Warning during initializeRun'); - } - if (failsDuringInit) { - throw ToolExit(2); - } - } - - @override - Future runForPackage(RepositoryPackage package) async { - checkedPackages.add(package.path); - final File warningFile = package.directory.childFile(_warningFile); - if (warningFile.existsSync()) { - final List warnings = warningFile.readAsLinesSync(); - warnings.forEach(logWarning); - } - final File skipFile = package.directory.childFile(_skipFile); - if (skipFile.existsSync()) { - return PackageResult.skip(skipFile.readAsStringSync()); - } - final File errorFile = package.directory.childFile(_errorFile); - if (errorFile.existsSync()) { - return PackageResult.fail(errorFile.readAsLinesSync()); - } - final File throwFile = package.directory.childFile(_throwFile); - if (throwFile.existsSync()) { - throw Exception('Uh-oh'); - } - return PackageResult.success(); - } - - @override - Future completeRun() async { - if (warnsDuringInit) { - logWarning('Warning during completeRun'); - } - } - - @override - Future handleCapturedOutput(List output) async { - capturedOutput.addAll(output); - } -} - -class MockProcessResult extends Mock implements io.ProcessResult {} diff --git a/script/tool/test/common/package_state_utils_test.dart b/script/tool/test/common/package_state_utils_test.dart deleted file mode 100644 index 86029cdf73a8..000000000000 --- a/script/tool/test/common/package_state_utils_test.dart +++ /dev/null @@ -1,344 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/git_version_finder.dart'; -import 'package:flutter_plugin_tools/src/common/package_state_utils.dart'; -import 'package:test/fake.dart'; -import 'package:test/test.dart'; - -import '../util.dart'; - -void main() { - late FileSystem fileSystem; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - }); - - group('checkPackageChangeState', () { - test('reports version change needed for code changes', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - const List changedFiles = [ - 'packages/a_package/lib/plugin.dart', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_package'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test('handles trailing slash on package path', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - const List changedFiles = [ - 'packages/a_package/lib/plugin.dart', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_package/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - expect(state.hasChangelogChange, false); - }); - - test('does not flag version- and changelog-change-exempt changes', - () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/CHANGELOG.md', - // Analysis. - 'packages/a_plugin/example/android/lint-baseline.xml', - // Tests. - 'packages/a_plugin/example/android/src/androidTest/foo/bar/FooTest.java', - 'packages/a_plugin/example/ios/RunnerTests/Foo.m', - 'packages/a_plugin/example/ios/RunnerUITests/info.plist', - // Test scripts. - 'packages/a_plugin/run_tests.sh', - // Tools. - 'packages/a_plugin/tool/a_development_tool.dart', - // Example build files. - 'packages/a_plugin/example/android/build.gradle', - 'packages/a_plugin/example/android/gradle/wrapper/gradle-wrapper.properties', - 'packages/a_plugin/example/ios/Runner.xcodeproj/project.pbxproj', - 'packages/a_plugin/example/linux/flutter/CMakeLists.txt', - 'packages/a_plugin/example/macos/Runner.xcodeproj/project.pbxproj', - 'packages/a_plugin/example/windows/CMakeLists.txt', - 'packages/a_plugin/example/pubspec.yaml', - // Pigeon platform tests, which have an unusual structure. - 'packages/a_plugin/platform_tests/shared_test_plugin_code/lib/integration_tests.dart', - 'packages/a_plugin/platform_tests/test_plugin/windows/test_plugin.cpp', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, false); - expect(state.needsChangelogChange, false); - expect(state.hasChangelogChange, true); - }); - - test('only considers a root "tool" folder to be special', () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/lib/foo/tool/tool_thing.dart', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test('requires a version change for example/lib/main.dart', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', packagesDir, - extraFiles: ['example/lib/main.dart']); - - const List changedFiles = [ - 'packages/a_plugin/example/lib/main.dart', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test('requires a version change for example/main.dart', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', packagesDir, - extraFiles: ['example/main.dart']); - - const List changedFiles = [ - 'packages/a_plugin/example/main.dart', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test('requires a version change for example readme.md', () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/example/README.md', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test('requires a version change for example/example.md', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', packagesDir, - extraFiles: ['example/example.md']); - - const List changedFiles = [ - 'packages/a_plugin/example/example.md', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test( - 'requires a changelog change but no version change for ' - 'lower-priority examples when example.md is present', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', packagesDir, - extraFiles: ['example/example.md']); - - const List changedFiles = [ - 'packages/a_plugin/example/lib/main.dart', - 'packages/a_plugin/example/main.dart', - 'packages/a_plugin/example/README.md', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, false); - expect(state.needsChangelogChange, true); - }); - - test( - 'requires a changelog change but no version change for README.md when ' - 'code example is present', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', packagesDir, - extraFiles: ['example/lib/main.dart']); - - const List changedFiles = [ - 'packages/a_plugin/example/README.md', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, false); - expect(state.needsChangelogChange, true); - }); - - test( - 'does not requires changelog or version change for build.gradle ' - 'test-dependency-only changes', () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/android/build.gradle', - ]; - - final GitVersionFinder git = FakeGitVersionFinder(>{ - 'packages/a_plugin/android/build.gradle': [ - "- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'", - "- testImplementation 'junit:junit:4.10.0'", - "+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'", - "+ testImplementation 'junit:junit:4.13.2'", - ] - }); - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/', - git: git); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, false); - expect(state.needsChangelogChange, false); - }); - - test('requires changelog or version change for other build.gradle changes', - () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/android/build.gradle', - ]; - - final GitVersionFinder git = FakeGitVersionFinder(>{ - 'packages/a_plugin/android/build.gradle': [ - "- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'", - "- testImplementation 'junit:junit:4.10.0'", - "+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'", - "+ testImplementation 'junit:junit:4.13.2'", - "- implementation 'com.google.android.gms:play-services-maps:18.0.0'", - "+ implementation 'com.google.android.gms:play-services-maps:18.0.2'", - ] - }); - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/', - git: git); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test( - 'requires changelog or version change if build.gradle diffs cannot ' - 'be checked', () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/android/build.gradle', - ]; - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/'); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - - test( - 'requires changelog or version change if build.gradle diffs cannot ' - 'be determined', () async { - final RepositoryPackage package = - createFakePlugin('a_plugin', packagesDir); - - const List changedFiles = [ - 'packages/a_plugin/android/build.gradle', - ]; - - final GitVersionFinder git = FakeGitVersionFinder(>{ - 'packages/a_plugin/android/build.gradle': [] - }); - - final PackageChangeState state = await checkPackageChangeState(package, - changedPaths: changedFiles, - relativePackagePath: 'packages/a_plugin/', - git: git); - - expect(state.hasChanges, true); - expect(state.needsVersionChange, true); - expect(state.needsChangelogChange, true); - }); - }); -} - -class FakeGitVersionFinder extends Fake implements GitVersionFinder { - FakeGitVersionFinder(this.fileDiffs); - - final Map> fileDiffs; - - @override - Future> getDiffContents({ - String? targetPath, - bool includeUncommitted = false, - }) async { - return fileDiffs[targetPath]!; - } -} diff --git a/script/tool/test/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart deleted file mode 100644 index 415b1db8932a..000000000000 --- a/script/tool/test/common/plugin_utils_test.dart +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:test/test.dart'; - -import '../util.dart'; - -void main() { - late FileSystem fileSystem; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - }); - - group('pluginSupportsPlatform', () { - test('no platforms', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); - - expect(pluginSupportsPlatform(platformAndroid, plugin), isFalse); - expect(pluginSupportsPlatform(platformIOS, plugin), isFalse); - expect(pluginSupportsPlatform(platformLinux, plugin), isFalse); - expect(pluginSupportsPlatform(platformMacOS, plugin), isFalse); - expect(pluginSupportsPlatform(platformWeb, plugin), isFalse); - expect(pluginSupportsPlatform(platformWindows, plugin), isFalse); - }); - - test('all platforms', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - platformLinux: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - platformWeb: const PlatformDetails(PlatformSupport.inline), - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - - expect(pluginSupportsPlatform(platformAndroid, plugin), isTrue); - expect(pluginSupportsPlatform(platformIOS, plugin), isTrue); - expect(pluginSupportsPlatform(platformLinux, plugin), isTrue); - expect(pluginSupportsPlatform(platformMacOS, plugin), isTrue); - expect(pluginSupportsPlatform(platformWeb, plugin), isTrue); - expect(pluginSupportsPlatform(platformWindows, plugin), isTrue); - }); - - test('some platforms', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformLinux: const PlatformDetails(PlatformSupport.inline), - platformWeb: const PlatformDetails(PlatformSupport.inline), - }); - - expect(pluginSupportsPlatform(platformAndroid, plugin), isTrue); - expect(pluginSupportsPlatform(platformIOS, plugin), isFalse); - expect(pluginSupportsPlatform(platformLinux, plugin), isTrue); - expect(pluginSupportsPlatform(platformMacOS, plugin), isFalse); - expect(pluginSupportsPlatform(platformWeb, plugin), isTrue); - expect(pluginSupportsPlatform(platformWindows, plugin), isFalse); - }); - - test('inline plugins are only detected as inline', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - platformLinux: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - platformWeb: const PlatformDetails(PlatformSupport.inline), - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - - expect( - pluginSupportsPlatform(platformAndroid, plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform(platformAndroid, plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform(platformIOS, plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform(platformIOS, plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform(platformLinux, plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform(platformLinux, plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform(platformMacOS, plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform(platformMacOS, plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform(platformWeb, plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform(platformWeb, plugin, - requiredMode: PlatformSupport.federated), - isFalse); - expect( - pluginSupportsPlatform(platformWindows, plugin, - requiredMode: PlatformSupport.inline), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - requiredMode: PlatformSupport.federated), - isFalse); - }); - - test('federated plugins are only detected as federated', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.federated), - platformIOS: const PlatformDetails(PlatformSupport.federated), - platformLinux: const PlatformDetails(PlatformSupport.federated), - platformMacOS: const PlatformDetails(PlatformSupport.federated), - platformWeb: const PlatformDetails(PlatformSupport.federated), - platformWindows: const PlatformDetails(PlatformSupport.federated), - }); - - expect( - pluginSupportsPlatform(platformAndroid, plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform(platformAndroid, plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform(platformIOS, plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform(platformIOS, plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform(platformLinux, plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform(platformLinux, plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform(platformMacOS, plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform(platformMacOS, plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform(platformWeb, plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform(platformWeb, plugin, - requiredMode: PlatformSupport.inline), - isFalse); - expect( - pluginSupportsPlatform(platformWindows, plugin, - requiredMode: PlatformSupport.federated), - isTrue); - expect( - pluginSupportsPlatform(platformWindows, plugin, - requiredMode: PlatformSupport.inline), - isFalse); - }); - }); - - group('pluginHasNativeCodeForPlatform', () { - test('returns false for web', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - expect(pluginHasNativeCodeForPlatform(platformWeb, plugin), isFalse); - }); - - test('returns false for a native-only plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - platformWindows: const PlatformDetails(PlatformSupport.inline), - }, - ); - - expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(platformWindows, plugin), isTrue); - }); - - test('returns true for a native+Dart plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline, hasDartCode: true), - platformMacOS: const PlatformDetails(PlatformSupport.inline, hasDartCode: true), - platformWindows: const PlatformDetails(PlatformSupport.inline, hasDartCode: true), - }, - ); - - expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isTrue); - expect(pluginHasNativeCodeForPlatform(platformWindows, plugin), isTrue); - }); - - test('returns false for a Dart-only plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline, - hasNativeCode: false, hasDartCode: true), - platformMacOS: const PlatformDetails(PlatformSupport.inline, - hasNativeCode: false, hasDartCode: true), - platformWindows: const PlatformDetails(PlatformSupport.inline, - hasNativeCode: false, hasDartCode: true), - }, - ); - - expect(pluginHasNativeCodeForPlatform(platformLinux, plugin), isFalse); - expect(pluginHasNativeCodeForPlatform(platformMacOS, plugin), isFalse); - expect(pluginHasNativeCodeForPlatform(platformWindows, plugin), isFalse); - }); - }); -} diff --git a/script/tool/test/common/pub_version_finder_test.dart b/script/tool/test/common/pub_version_finder_test.dart deleted file mode 100644 index 1692cf214abe..000000000000 --- a/script/tool/test/common/pub_version_finder_test.dart +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io'; - -import 'package:flutter_plugin_tools/src/common/pub_version_finder.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; -import 'package:mockito/mockito.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:test/test.dart'; - -void main() { - test('Package does not exist.', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('', 404); - }); - final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); - final PubVersionFinderResponse response = - await finder.getPackageVersion(packageName: 'some_package'); - - expect(response.versions, isEmpty); - expect(response.result, PubVersionFinderResult.noPackageFound); - expect(response.httpResponse.statusCode, 404); - expect(response.httpResponse.body, ''); - }); - - test('HTTP error when getting versions from pub', () async { - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response('', 400); - }); - final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); - final PubVersionFinderResponse response = - await finder.getPackageVersion(packageName: 'some_package'); - - expect(response.versions, isEmpty); - expect(response.result, PubVersionFinderResult.fail); - expect(response.httpResponse.statusCode, 400); - expect(response.httpResponse.body, ''); - }); - - test('Get a correct list of versions when http response is OK.', () async { - const Map httpResponse = { - 'name': 'some_package', - 'versions': [ - '0.0.1', - '0.0.2', - '0.0.2+2', - '0.1.1', - '0.0.1+1', - '0.1.0', - '0.2.0', - '0.1.0+1', - '0.0.2+1', - '2.0.0', - '1.2.0', - '1.0.0', - ], - }; - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response(json.encode(httpResponse), 200); - }); - final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); - final PubVersionFinderResponse response = - await finder.getPackageVersion(packageName: 'some_package'); - - expect(response.versions, [ - Version.parse('2.0.0'), - Version.parse('1.2.0'), - Version.parse('1.0.0'), - Version.parse('0.2.0'), - Version.parse('0.1.1'), - Version.parse('0.1.0+1'), - Version.parse('0.1.0'), - Version.parse('0.0.2+2'), - Version.parse('0.0.2+1'), - Version.parse('0.0.2'), - Version.parse('0.0.1+1'), - Version.parse('0.0.1'), - ]); - expect(response.result, PubVersionFinderResult.success); - expect(response.httpResponse.statusCode, 200); - expect(response.httpResponse.body, json.encode(httpResponse)); - }); -} - -class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart deleted file mode 100644 index db519c008233..000000000000 --- a/script/tool/test/common/repository_package_test.dart +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:test/test.dart'; - -import '../util.dart'; - -void main() { - late FileSystem fileSystem; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - }); - - group('displayName', () { - test('prints packageDir-relative paths by default', () async { - expect( - RepositoryPackage(packagesDir.childDirectory('foo')).displayName, - 'foo', - ); - expect( - RepositoryPackage(packagesDir - .childDirectory('foo') - .childDirectory('bar') - .childDirectory('baz')) - .displayName, - 'foo/bar/baz', - ); - }); - - test('handles third_party/packages/', () async { - expect( - RepositoryPackage(packagesDir.parent - .childDirectory('third_party') - .childDirectory('packages') - .childDirectory('foo') - .childDirectory('bar') - .childDirectory('baz')) - .displayName, - 'foo/bar/baz', - ); - }); - - test('always uses Posix-style paths', () async { - final Directory windowsPackagesDir = createPackagesDirectory( - fileSystem: MemoryFileSystem(style: FileSystemStyle.windows)); - - expect( - RepositoryPackage(windowsPackagesDir.childDirectory('foo')).displayName, - 'foo', - ); - expect( - RepositoryPackage(windowsPackagesDir - .childDirectory('foo') - .childDirectory('bar') - .childDirectory('baz')) - .displayName, - 'foo/bar/baz', - ); - }); - - test('elides group name in grouped federated plugin structure', () async { - expect( - RepositoryPackage(packagesDir - .childDirectory('a_plugin') - .childDirectory('a_plugin_platform_interface')) - .displayName, - 'a_plugin_platform_interface', - ); - expect( - RepositoryPackage(packagesDir - .childDirectory('a_plugin') - .childDirectory('a_plugin_platform_web')) - .displayName, - 'a_plugin_platform_web', - ); - }); - - // The app-facing package doesn't get elided to avoid potential confusion - // with the group folder itself. - test('does not elide group name for app-facing packages', () async { - expect( - RepositoryPackage(packagesDir - .childDirectory('a_plugin') - .childDirectory('a_plugin')) - .displayName, - 'a_plugin/a_plugin', - ); - }); - }); - - group('getExamples', () { - test('handles a single Flutter example', () async { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir); - - final List examples = plugin.getExamples().toList(); - - expect(examples.length, 1); - expect(examples[0].path, getExampleDir(plugin).path); - }); - - test('handles multiple Flutter examples', () async { - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, - examples: ['example1', 'example2']); - - final List examples = plugin.getExamples().toList(); - - expect(examples.length, 2); - expect(examples[0].path, - getExampleDir(plugin).childDirectory('example1').path); - expect(examples[1].path, - getExampleDir(plugin).childDirectory('example2').path); - }); - - test('handles a single non-Flutter example', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - final List examples = package.getExamples().toList(); - - expect(examples.length, 1); - expect(examples[0].path, getExampleDir(package).path); - }); - - test('handles multiple non-Flutter examples', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - examples: ['example1', 'example2']); - - final List examples = package.getExamples().toList(); - - expect(examples.length, 2); - expect(examples[0].path, - getExampleDir(package).childDirectory('example1').path); - expect(examples[1].path, - getExampleDir(package).childDirectory('example2').path); - }); - }); - - group('federated plugin queries', () { - test('all return false for a simple plugin', () { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir); - expect(plugin.isFederated, false); - expect(plugin.isAppFacing, false); - expect(plugin.isPlatformInterface, false); - expect(plugin.isFederated, false); - }); - - test('handle app-facing packages', () { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); - expect(plugin.isFederated, true); - expect(plugin.isAppFacing, true); - expect(plugin.isPlatformInterface, false); - expect(plugin.isPlatformImplementation, false); - }); - - test('handle platform interface packages', () { - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin_platform_interface', - packagesDir.childDirectory('a_plugin')); - expect(plugin.isFederated, true); - expect(plugin.isAppFacing, false); - expect(plugin.isPlatformInterface, true); - expect(plugin.isPlatformImplementation, false); - }); - - test('handle platform implementation packages', () { - // A platform interface can end with anything, not just one of the known - // platform names, because of cases like webview_flutter_wkwebview. - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin_foo', packagesDir.childDirectory('a_plugin')); - expect(plugin.isFederated, true); - expect(plugin.isAppFacing, false); - expect(plugin.isPlatformInterface, false); - expect(plugin.isPlatformImplementation, true); - }); - }); - - group('pubspec', () { - test('file', () async { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir); - - final File pubspecFile = plugin.pubspecFile; - - expect(pubspecFile.path, plugin.directory.childFile('pubspec.yaml').path); - }); - - test('parsing', () async { - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, - examples: ['example1', 'example2']); - - final Pubspec pubspec = plugin.parsePubspec(); - - expect(pubspec.name, 'a_plugin'); - }); - }); - - group('requiresFlutter', () { - test('returns true for Flutter package', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, isFlutter: true); - expect(package.requiresFlutter(), true); - }); - - test('returns false for non-Flutter package', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - expect(package.requiresFlutter(), false); - }); - }); -} diff --git a/script/tool/test/common/xcode_test.dart b/script/tool/test/common/xcode_test.dart deleted file mode 100644 index 259d8ea36cd2..000000000000 --- a/script/tool/test/common/xcode_test.dart +++ /dev/null @@ -1,406 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:flutter_plugin_tools/src/common/xcode.dart'; -import 'package:test/test.dart'; - -import '../mocks.dart'; -import '../util.dart'; - -void main() { - late RecordingProcessRunner processRunner; - late Xcode xcode; - - setUp(() { - processRunner = RecordingProcessRunner(); - xcode = Xcode(processRunner: processRunner); - }); - - group('findBestAvailableIphoneSimulator', () { - test('finds the newest device', () async { - const String expectedDeviceId = '1E76A0FD-38AC-4537-A989-EA639D7D012A'; - // Note: This uses `dynamic` deliberately, and should not be updated to - // Object, in order to ensure that the code correctly handles this return - // type from JSON decoding. - final Map devices = { - 'runtimes': >[ - { - 'bundlePath': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime', - 'buildversion': '17A577', - 'runtimeRoot': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-0', - 'version': '13.0', - 'isAvailable': true, - 'name': 'iOS 13.0' - }, - { - 'bundlePath': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', - 'buildversion': '17L255', - 'runtimeRoot': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', - 'version': '13.4', - 'isAvailable': true, - 'name': 'iOS 13.4' - }, - { - 'bundlePath': - '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', - 'buildversion': '17T531', - 'runtimeRoot': - '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', - 'version': '6.2.1', - 'isAvailable': true, - 'name': 'watchOS 6.2' - } - ], - 'devices': { - 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ - { - 'dataPath': - '/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data', - 'logPath': - '/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774', - 'udid': '2706BBEB-1E01-403E-A8E9-70E8E5A24774', - 'isAvailable': true, - 'deviceTypeIdentifier': - 'com.apple.CoreSimulator.SimDeviceType.iPhone-8', - 'state': 'Shutdown', - 'name': 'iPhone 8' - }, - { - 'dataPath': - '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', - 'logPath': - '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'udid': expectedDeviceId, - 'isAvailable': true, - 'deviceTypeIdentifier': - 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', - 'state': 'Shutdown', - 'name': 'iPhone 8 Plus' - } - ] - } - }; - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: jsonEncode(devices)), - ]; - - expect(await xcode.findBestAvailableIphoneSimulator(), expectedDeviceId); - }); - - test('ignores non-iOS runtimes', () async { - // Note: This uses `dynamic` deliberately, and should not be updated to - // Object, in order to ensure that the code correctly handles this return - // type from JSON decoding. - final Map devices = { - 'runtimes': >[ - { - 'bundlePath': - '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', - 'buildversion': '17T531', - 'runtimeRoot': - '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', - 'version': '6.2.1', - 'isAvailable': true, - 'name': 'watchOS 6.2' - } - ], - 'devices': { - 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2': - >[ - { - 'dataPath': - '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', - 'logPath': - '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'isAvailable': true, - 'deviceTypeIdentifier': - 'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm', - 'state': 'Shutdown', - 'name': 'Apple Watch' - } - ] - } - }; - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: jsonEncode(devices)), - ]; - - expect(await xcode.findBestAvailableIphoneSimulator(), null); - }); - - test('returns null if simctl fails', () async { - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1), - ]; - - expect(await xcode.findBestAvailableIphoneSimulator(), null); - }); - }); - - group('runXcodeBuild', () { - test('handles minimal arguments', () async { - final Directory directory = const LocalFileSystem().currentDirectory; - - final int exitCode = await xcode.runXcodeBuild( - directory, - workspace: 'A.xcworkspace', - scheme: 'AScheme', - ); - - expect(exitCode, 0); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'build', - '-workspace', - 'A.xcworkspace', - '-scheme', - 'AScheme', - ], - directory.path), - ])); - }); - - test('handles all arguments', () async { - final Directory directory = const LocalFileSystem().currentDirectory; - - final int exitCode = await xcode.runXcodeBuild(directory, - actions: ['action1', 'action2'], - workspace: 'A.xcworkspace', - scheme: 'AScheme', - configuration: 'Debug', - extraFlags: ['-a', '-b', 'c=d']); - - expect(exitCode, 0); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'action1', - 'action2', - '-workspace', - 'A.xcworkspace', - '-scheme', - 'AScheme', - '-configuration', - 'Debug', - '-a', - '-b', - 'c=d', - ], - directory.path), - ])); - }); - - test('returns error codes', () async { - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1), - ]; - final Directory directory = const LocalFileSystem().currentDirectory; - - final int exitCode = await xcode.runXcodeBuild( - directory, - workspace: 'A.xcworkspace', - scheme: 'AScheme', - ); - - expect(exitCode, 1); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'build', - '-workspace', - 'A.xcworkspace', - '-scheme', - 'AScheme', - ], - directory.path), - ])); - }); - }); - - group('projectHasTarget', () { - test('returns true when present', () async { - const String stdout = ''' -{ - "project" : { - "configurations" : [ - "Debug", - "Release" - ], - "name" : "Runner", - "schemes" : [ - "Runner" - ], - "targets" : [ - "Runner", - "RunnerTests", - "RunnerUITests" - ] - } -}'''; - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: stdout), - ]; - - final Directory project = - const LocalFileSystem().directory('/foo.xcodeproj'); - expect(await xcode.projectHasTarget(project, 'RunnerTests'), true); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - project.path, - ], - null), - ])); - }); - - test('returns false when not present', () async { - const String stdout = ''' -{ - "project" : { - "configurations" : [ - "Debug", - "Release" - ], - "name" : "Runner", - "schemes" : [ - "Runner" - ], - "targets" : [ - "Runner", - "RunnerUITests" - ] - } -}'''; - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: stdout), - ]; - - final Directory project = - const LocalFileSystem().directory('/foo.xcodeproj'); - expect(await xcode.projectHasTarget(project, 'RunnerTests'), false); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - project.path, - ], - null), - ])); - }); - - test('returns null for unexpected output', () async { - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: '{}'), - ]; - - final Directory project = - const LocalFileSystem().directory('/foo.xcodeproj'); - expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - project.path, - ], - null), - ])); - }); - - test('returns null for invalid output', () async { - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: ':)'), - ]; - - final Directory project = - const LocalFileSystem().directory('/foo.xcodeproj'); - expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - project.path, - ], - null), - ])); - }); - - test('returns null for failure', () async { - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1), // xcodebuild -list - ]; - - final Directory project = - const LocalFileSystem().directory('/foo.xcodeproj'); - expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - project.path, - ], - null), - ])); - }); - }); -} diff --git a/script/tool/test/create_all_packages_app_command_test.dart b/script/tool/test/create_all_packages_app_command_test.dart deleted file mode 100644 index 54551cbc3712..000000000000 --- a/script/tool/test/create_all_packages_app_command_test.dart +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart: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/common/core.dart'; -import 'package:flutter_plugin_tools/src/create_all_packages_app_command.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late CommandRunner runner; - late CreateAllPackagesAppCommand command; - late FileSystem fileSystem; - late Directory testRoot; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - - setUp(() { - // Since the core of this command is a call to 'flutter create', the test - // has to use the real filesystem. Put everything possible in a unique - // temporary to minimize effect on the host system. - fileSystem = const LocalFileSystem(); - testRoot = fileSystem.systemTempDirectory.createTempSync(); - packagesDir = testRoot.childDirectory('packages'); - processRunner = RecordingProcessRunner(); - - command = CreateAllPackagesAppCommand( - packagesDir, - processRunner: processRunner, - pluginsRoot: testRoot, - ); - runner = CommandRunner( - 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); - runner.addCommand(command); - }); - - tearDown(() { - testRoot.deleteSync(recursive: true); - }); - - group('non-macOS host', () { - setUp(() { - command = CreateAllPackagesAppCommand( - packagesDir, - processRunner: processRunner, - // Set isWindows or not based on the actual host, so that - // `flutterCommand` works, since these tests actually call 'flutter'. - // The important thing is that isMacOS always returns false. - platform: MockPlatform(isWindows: const LocalPlatform().isWindows), - pluginsRoot: testRoot, - ); - runner = CommandRunner( - 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); - runner.addCommand(command); - }); - - test('pubspec includes all plugins', () async { - createFakePlugin('plugina', packagesDir); - createFakePlugin('pluginb', packagesDir); - createFakePlugin('pluginc', packagesDir); - - await runCapturingPrint(runner, ['create-all-packages-app']); - final List pubspec = command.app.pubspecFile.readAsLinesSync(); - - expect( - pubspec, - containsAll([ - contains(RegExp('path: .*/packages/plugina')), - contains(RegExp('path: .*/packages/pluginb')), - contains(RegExp('path: .*/packages/pluginc')), - ])); - }); - - test('pubspec has overrides for all plugins', () async { - createFakePlugin('plugina', packagesDir); - createFakePlugin('pluginb', packagesDir); - createFakePlugin('pluginc', packagesDir); - - await runCapturingPrint(runner, ['create-all-packages-app']); - final List pubspec = command.app.pubspecFile.readAsLinesSync(); - - expect( - pubspec, - containsAllInOrder([ - contains('dependency_overrides:'), - contains(RegExp('path: .*/packages/plugina')), - contains(RegExp('path: .*/packages/pluginb')), - contains(RegExp('path: .*/packages/pluginc')), - ])); - }); - - test('pubspec preserves existing Dart SDK version', () async { - const String baselineProjectName = 'baseline'; - final Directory baselineProjectDirectory = - testRoot.childDirectory(baselineProjectName); - io.Process.runSync( - getFlutterCommand(const LocalPlatform()), - [ - 'create', - '--template=app', - '--project-name=$baselineProjectName', - baselineProjectDirectory.path, - ], - ); - final Pubspec baselinePubspec = - RepositoryPackage(baselineProjectDirectory).parsePubspec(); - - createFakePlugin('plugina', packagesDir); - - await runCapturingPrint(runner, ['create-all-packages-app']); - final Pubspec generatedPubspec = command.app.parsePubspec(); - - const String dartSdkKey = 'sdk'; - expect(generatedPubspec.environment?[dartSdkKey], - baselinePubspec.environment?[dartSdkKey]); - }); - - test('macOS deployment target is modified in pbxproj', () async { - createFakePlugin('plugina', packagesDir); - - await runCapturingPrint(runner, ['create-all-packages-app']); - final List pbxproj = command.app - .platformDirectory(FlutterPlatform.macos) - .childDirectory('Runner.xcodeproj') - .childFile('project.pbxproj') - .readAsLinesSync(); - - expect( - pbxproj, - everyElement((String line) => - !line.contains('MACOSX_DEPLOYMENT_TARGET') || - line.contains('10.15'))); - }); - - test('calls flutter pub get', () async { - createFakePlugin('plugina', packagesDir); - - await runCapturingPrint(runner, ['create-all-packages-app']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(const LocalPlatform()), - const ['pub', 'get'], - testRoot.childDirectory('all_packages').path), - ])); - }, - // See comment about Windows in create_all_packages_app_command.dart - skip: io.Platform.isWindows); - - test('fails if flutter pub get fails', () async { - createFakePlugin('plugina', packagesDir); - - processRunner.mockProcessesForExecutable[ - getFlutterCommand(const LocalPlatform())] = [ - MockProcess(exitCode: 1) - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['create-all-packages-app'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - "Failed to generate native build files via 'flutter pub get'"), - ])); - }, - // See comment about Windows in create_all_packages_app_command.dart - skip: io.Platform.isWindows); - - test('handles --output-dir', () async { - createFakePlugin('plugina', packagesDir); - - final Directory customOutputDir = - fileSystem.systemTempDirectory.createTempSync(); - await runCapturingPrint(runner, [ - 'create-all-packages-app', - '--output-dir=${customOutputDir.path}' - ]); - - expect(command.app.path, - customOutputDir.childDirectory('all_packages').path); - }); - - test('logs exclusions', () async { - createFakePlugin('plugina', packagesDir); - createFakePlugin('pluginb', packagesDir); - createFakePlugin('pluginc', packagesDir); - - final List output = await runCapturingPrint(runner, - ['create-all-packages-app', '--exclude=pluginb,pluginc']); - - expect( - output, - containsAllInOrder([ - 'Exluding the following plugins from the combined build:', - ' pluginb', - ' pluginc', - ])); - }); - }); - - group('macOS host', () { - setUp(() { - command = CreateAllPackagesAppCommand( - packagesDir, - processRunner: processRunner, - platform: MockPlatform(isMacOS: true), - pluginsRoot: testRoot, - ); - runner = CommandRunner( - 'create_all_test', 'Test for $CreateAllPackagesAppCommand'); - runner.addCommand(command); - }); - - test('macOS deployment target is modified in Podfile', () async { - createFakePlugin('plugina', packagesDir); - - final File podfileFile = RepositoryPackage( - command.packagesDir.parent.childDirectory('all_packages')) - .platformDirectory(FlutterPlatform.macos) - .childFile('Podfile'); - podfileFile.createSync(recursive: true); - podfileFile.writeAsStringSync(""" -platform :osx, '10.11' -# some other line -"""); - - await runCapturingPrint(runner, ['create-all-packages-app']); - final List podfile = command.app - .platformDirectory(FlutterPlatform.macos) - .childFile('Podfile') - .readAsLinesSync(); - - expect( - podfile, - everyElement((String line) => - !line.contains('platform :osx') || line.contains("'10.15'"))); - }, - // Podfile is only generated (and thus only edited) on macOS. - skip: !io.Platform.isMacOS); - }); -} diff --git a/script/tool/test/custom_test_command_test.dart b/script/tool/test/custom_test_command_test.dart deleted file mode 100644 index 8b0c021b1255..000000000000 --- a/script/tool/test/custom_test_command_test.dart +++ /dev/null @@ -1,328 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/custom_test_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - late CommandRunner runner; - - group('posix', () { - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final CustomTestCommand analyzeCommand = CustomTestCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'custom_test_command', 'Test for custom_test_command'); - runner.addCommand(analyzeCommand); - }); - - test('runs both new and legacy when both are present', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, extraFiles: [ - 'tool/run_tests.dart', - 'run_tests.sh', - ]); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall(package.directory.childFile('run_tests.sh').path, - const [], package.path), - ProcessCall('dart', const ['run', 'tool/run_tests.dart'], - package.path), - ])); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('runs when only new is present', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - extraFiles: ['tool/run_tests.dart']); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall('dart', const ['run', 'tool/run_tests.dart'], - package.path), - ])); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('runs pub get before running Dart test script', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - extraFiles: ['tool/run_tests.dart']); - - await runCapturingPrint(runner, ['custom-test']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall('dart', const ['pub', 'get'], package.path), - ProcessCall('dart', const ['run', 'tool/run_tests.dart'], - package.path), - ])); - }); - - test('runs when only legacy is present', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - extraFiles: ['run_tests.sh']); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall(package.directory.childFile('run_tests.sh').path, - const [], package.path), - ])); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('skips when neither is present', () async { - createFakePackage('a_package', packagesDir); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect(processRunner.recordedCalls, isEmpty); - - expect( - output, - containsAllInOrder([ - contains('Skipped 1 package(s)'), - ])); - }); - - test('fails if new fails', () async { - createFakePackage('a_package', packagesDir, extraFiles: [ - 'tool/run_tests.dart', - 'run_tests.sh', - ]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(), // pub get - MockProcess(exitCode: 1), // test script - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['custom-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package') - ])); - }); - - test('fails if pub get fails', () async { - createFakePackage('a_package', packagesDir, extraFiles: [ - 'tool/run_tests.dart', - 'run_tests.sh', - ]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['custom-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package:\n' - ' Unable to get script dependencies') - ])); - }); - - test('fails if legacy fails', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, extraFiles: [ - 'tool/run_tests.dart', - 'run_tests.sh', - ]); - - processRunner.mockProcessesForExecutable[ - package.directory.childFile('run_tests.sh').path] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['custom-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package') - ])); - }); - }); - - group('Windows', () { - setUp(() { - fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); - mockPlatform = MockPlatform(isWindows: true); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final CustomTestCommand analyzeCommand = CustomTestCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'custom_test_command', 'Test for custom_test_command'); - runner.addCommand(analyzeCommand); - }); - - test('runs new and skips old when both are present', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, extraFiles: [ - 'tool/run_tests.dart', - 'run_tests.sh', - ]); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall('dart', const ['run', 'tool/run_tests.dart'], - package.path), - ])); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('runs when only new is present', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - extraFiles: ['tool/run_tests.dart']); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall('dart', const ['run', 'tool/run_tests.dart'], - package.path), - ])); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('skips package when only legacy is present', () async { - createFakePackage('a_package', packagesDir, - extraFiles: ['run_tests.sh']); - - final List output = - await runCapturingPrint(runner, ['custom-test']); - - expect(processRunner.recordedCalls, isEmpty); - - expect( - output, - containsAllInOrder([ - contains('run_tests.sh is not supported on Windows'), - contains('Skipped 1 package(s)'), - ])); - }); - - test('fails if new fails', () async { - createFakePackage('a_package', packagesDir, extraFiles: [ - 'tool/run_tests.dart', - 'run_tests.sh', - ]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(), // pub get - MockProcess(exitCode: 1), // test script - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['custom-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package') - ])); - }); - }); -} diff --git a/script/tool/test/dependabot_check_command_test.dart b/script/tool/test/dependabot_check_command_test.dart deleted file mode 100644 index 39dd8f4fcb92..000000000000 --- a/script/tool/test/dependabot_check_command_test.dart +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/dependabot_check_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'util.dart'; - -void main() { - late CommandRunner runner; - late FileSystem fileSystem; - late Directory root; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - root = fileSystem.currentDirectory; - packagesDir = root.childDirectory('packages'); - - final MockGitDir gitDir = MockGitDir(); - when(gitDir.path).thenReturn(root.path); - - final DependabotCheckCommand command = DependabotCheckCommand( - packagesDir, - gitDir: gitDir, - ); - runner = CommandRunner( - 'dependabot_test', 'Test for $DependabotCheckCommand'); - runner.addCommand(command); - }); - - void setDependabotCoverage({ - Iterable gradleDirs = const [], - }) { - final Iterable gradleEntries = - gradleDirs.map((String directory) => ''' - - package-ecosystem: "gradle" - directory: "/$directory" - schedule: - interval: "daily" -'''); - final File configFile = - root.childDirectory('.github').childFile('dependabot.yml'); - configFile.createSync(recursive: true); - configFile.writeAsStringSync(''' -version: 2 -updates: -${gradleEntries.join('\n')} -'''); - } - - test('skips with no supported ecosystems', () async { - setDependabotCoverage(); - createFakePackage('a_package', packagesDir); - - final List output = - await runCapturingPrint(runner, ['dependabot-check']); - - expect( - output, - containsAllInOrder([ - contains('SKIPPING: No supported package ecosystems'), - ])); - }); - - test('fails for app missing Gradle coverage', () async { - setDependabotCoverage(); - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - package.directory - .childDirectory('example') - .childDirectory('android') - .childDirectory('app') - .createSync(recursive: true); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['dependabot-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Missing Gradle coverage.'), - contains('a_package/example:\n' - ' Missing Gradle coverage') - ])); - }); - - test('fails for plugin missing Gradle coverage', () async { - setDependabotCoverage(); - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir); - plugin.directory.childDirectory('android').createSync(recursive: true); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['dependabot-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Missing Gradle coverage.'), - contains('a_plugin:\n' - ' Missing Gradle coverage') - ])); - }); - - test('passes for correct Gradle coverage', () async { - setDependabotCoverage(gradleDirs: [ - 'packages/a_plugin/android', - 'packages/a_plugin/example/android/app', - ]); - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir); - // Test the plugin. - plugin.directory.childDirectory('android').createSync(recursive: true); - // And its example app. - plugin.directory - .childDirectory('example') - .childDirectory('android') - .childDirectory('app') - .createSync(recursive: true); - - final List output = - await runCapturingPrint(runner, ['dependabot-check']); - - expect(output, - containsAllInOrder([contains('Ran for 2 package(s)')])); - }); -} diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart deleted file mode 100644 index 0b6082098ae8..000000000000 --- a/script/tool/test/drive_examples_command_test.dart +++ /dev/null @@ -1,1257 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -const String _fakeIOSDevice = '67d5c3d1-8bdf-46ad-8f6b-b00e2a972dda'; -const String _fakeAndroidDevice = 'emulator-1234'; - -void main() { - group('test drive_example_command', () { - late FileSystem fileSystem; - late Platform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final DriveExamplesCommand command = DriveExamplesCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform); - - runner = CommandRunner( - 'drive_examples_command', 'Test for drive_example_command'); - runner.addCommand(command); - }); - - void setMockFlutterDevicesOutput({ - bool hasIOSDevice = true, - bool hasAndroidDevice = true, - bool includeBanner = false, - }) { - const String updateBanner = ''' -╔════════════════════════════════════════════════════════════════════════════╗ -║ A new version of Flutter is available! ║ -║ ║ -║ To update to the latest version, run "flutter upgrade". ║ -╚════════════════════════════════════════════════════════════════════════════╝ -'''; - final List devices = [ - if (hasIOSDevice) '{"id": "$_fakeIOSDevice", "targetPlatform": "ios"}', - if (hasAndroidDevice) - '{"id": "$_fakeAndroidDevice", "targetPlatform": "android-x86"}', - ]; - final String output = - '''${includeBanner ? updateBanner : ''}[${devices.join(',')}]'''; - - final MockProcess mockDevicesProcess = - MockProcess(stdout: output, stdoutEncoding: utf8); - processRunner - .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [mockDevicesProcess]; - } - - test('fails if no platforms are provided', () async { - setMockFlutterDevicesOutput(); - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Exactly one of'), - ]), - ); - }); - - test('fails if multiple platforms are provided', () async { - setMockFlutterDevicesOutput(); - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--ios', '--macos'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Exactly one of'), - ]), - ); - }); - - test('fails for iOS if no iOS devices are present', () async { - setMockFlutterDevicesOutput(hasIOSDevice: false); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--ios'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No iOS devices'), - ]), - ); - }); - - test('handles flutter tool banners when checking devices', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/integration_test.dart', - 'example/integration_test/foo_test.dart', - 'example/ios/ios.m', - ], - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - setMockFlutterDevicesOutput(includeBanner: true); - final List output = - await runCapturingPrint(runner, ['drive-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - }); - - test('fails for iOS if getting devices fails', () async { - // Simulate failure from `flutter devices`. - processRunner - .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [MockProcess(exitCode: 1)]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--ios'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No iOS devices'), - ]), - ); - }); - - test('fails for Android if no Android devices are present', () async { - setMockFlutterDevicesOutput(hasAndroidDevice: false); - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No Android devices'), - ]), - ); - }); - - test('driving under folder "test_driver"', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/android/android.java', - 'example/ios/ios.m', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - setMockFlutterDevicesOutput(); - final List output = - await runCapturingPrint(runner, ['drive-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['devices', '--machine'], null), - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - _fakeIOSDevice, - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving under folder "test_driver" when test files are missing"', - () async { - setMockFlutterDevicesOutput(); - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/android/android.java', - 'example/ios/ios.m', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No driver tests were run (1 example(s) found).'), - contains('No test files for example/test_driver/plugin_test.dart'), - ]), - ); - }); - - test('a plugin without any integration test files is reported as an error', - () async { - setMockFlutterDevicesOutput(); - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/lib/main.dart', - 'example/android/android.java', - 'example/ios/ios.m', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No driver tests were run (1 example(s) found).'), - contains('No tests ran'), - ]), - ); - }); - - test('integration tests using test(...) fail validation', () async { - setMockFlutterDevicesOutput(); - final RepositoryPackage package = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/integration_test.dart', - 'example/integration_test/foo_test.dart', - 'example/android/android.java', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - package.directory - .childDirectory('example') - .childDirectory('integration_test') - .childFile('foo_test.dart') - .writeAsStringSync(''' - test('this is the wrong kind of test!'), () { - ... - } -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('foo_test.dart failed validation'), - ]), - ); - }); - - test( - 'driving under folder "test_driver" when targets are under "integration_test"', - () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/integration_test.dart', - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/integration_test/ignore_me.dart', - 'example/android/android.java', - 'example/ios/ios.m', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - setMockFlutterDevicesOutput(); - final List output = - await runCapturingPrint(runner, ['drive-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['devices', '--machine'], null), - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - _fakeIOSDevice, - '--driver', - 'test_driver/integration_test.dart', - '--target', - 'integration_test/bar_test.dart', - ], - pluginExampleDirectory.path), - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - _fakeIOSDevice, - '--driver', - 'test_driver/integration_test.dart', - '--target', - 'integration_test/foo_test.dart', - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not support Linux is a no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ]); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--linux', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Skipping unsupported platform linux...'), - contains('No issues found!'), - ]), - ); - - // Output should be empty since running drive-examples --linux on a non-Linux - // plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving on a Linux plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/linux/linux.cc', - ], - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--linux', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'linux', - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not suppport macOS is a no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ]); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--macos', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Skipping unsupported platform macos...'), - contains('No issues found!'), - ]), - ); - - // Output should be empty since running drive-examples --macos with no macos - // implementation is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving on a macOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/macos/macos.swift', - ], - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--macos', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'macos', - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not suppport web is a no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ]); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - // Output should be empty since running drive-examples --web on a non-web - // plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving a web plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/web/index.html', - ], - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'web-server', - '--web-port=7357', - '--browser-name=chrome', - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving a web plugin with CHROME_EXECUTABLE', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/web/index.html', - ], - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - mockPlatform.environment['CHROME_EXECUTABLE'] = '/path/to/chrome'; - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'web-server', - '--web-port=7357', - '--browser-name=chrome', - '--chrome-binary=/path/to/chrome', - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not suppport Windows is a no-op', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ]); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--windows', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Skipping unsupported platform windows...'), - contains('No issues found!'), - ]), - ); - - // Output should be empty since running drive-examples --windows on a - // non-Windows plugin is a no-op. - expect(processRunner.recordedCalls, []); - }); - - test('driving on a Windows plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/windows/windows.cpp', - ], - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--windows', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'windows', - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving on an Android plugin', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/android/android.java', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - setMockFlutterDevicesOutput(); - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--android', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['devices', '--machine'], null), - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - _fakeAndroidDevice, - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('driving when plugin does not support Android is no-op', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ], - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - setMockFlutterDevicesOutput(); - final List output = await runCapturingPrint( - runner, ['drive-examples', '--android']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Skipping unsupported platform android...'), - contains('No issues found!'), - ]), - ); - - // Output should be empty other than the device query. - expect(processRunner.recordedCalls, [ - ProcessCall(getFlutterCommand(mockPlatform), - const ['devices', '--machine'], null), - ]); - }); - - test('driving when plugin does not support iOS is no-op', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - ], - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - setMockFlutterDevicesOutput(); - final List output = - await runCapturingPrint(runner, ['drive-examples', '--ios']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Skipping unsupported platform ios...'), - contains('No issues found!'), - ]), - ); - - // Output should be empty other than the device query. - expect(processRunner.recordedCalls, [ - ProcessCall(getFlutterCommand(mockPlatform), - const ['devices', '--machine'], null), - ]); - }); - - test('platform interface plugins are silently skipped', () async { - createFakePlugin('aplugin_platform_interface', packagesDir, - examples: []); - - setMockFlutterDevicesOutput(); - final List output = await runCapturingPrint( - runner, ['drive-examples', '--macos']); - - expect( - output, - containsAllInOrder([ - contains('Running for aplugin_platform_interface'), - contains( - 'SKIPPING: Platform interfaces are not expected to have integration tests.'), - contains('No issues found!'), - ]), - ); - - // Output should be empty since it's skipped. - expect(processRunner.recordedCalls, []); - }); - - test('enable-experiment flag', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/plugin_test.dart', - 'example/test_driver/plugin.dart', - 'example/android/android.java', - 'example/ios/ios.m', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - setMockFlutterDevicesOutput(); - await runCapturingPrint(runner, [ - 'drive-examples', - '--ios', - '--enable-experiment=exp1', - ]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['devices', '--machine'], null), - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - _fakeIOSDevice, - '--enable-experiment=exp1', - '--driver', - 'test_driver/plugin_test.dart', - '--target', - 'test_driver/plugin.dart' - ], - pluginExampleDirectory.path), - ])); - }); - - test('fails when no example is present', () async { - createFakePlugin( - 'plugin', - packagesDir, - examples: [], - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--web'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No driver tests were run (0 example(s) found).'), - contains('The following packages had errors:'), - contains(' plugin:\n' - ' No tests ran (use --exclude if this is intentional)'), - ]), - ); - }); - - test('fails when no driver is present', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/web/index.html', - ], - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--web'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No driver tests found for plugin/example'), - contains('No driver tests were run (1 example(s) found).'), - contains('The following packages had errors:'), - contains(' plugin:\n' - ' No tests ran (use --exclude if this is intentional)'), - ]), - ); - }); - - test('fails when no integration tests are present', () async { - createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/integration_test.dart', - 'example/web/index.html', - ], - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['drive-examples', '--web'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Found example/test_driver/integration_test.dart, but no ' - 'integration_test/*_test.dart files.'), - contains('No driver tests were run (1 example(s) found).'), - contains('The following packages had errors:'), - contains(' plugin:\n' - ' No test files for example/test_driver/integration_test.dart\n' - ' No tests ran (use --exclude if this is intentional)'), - ]), - ); - }); - - test('reports test failures', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/test_driver/integration_test.dart', - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/macos/macos.swift', - ], - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - // Simulate failure from `flutter drive`. - processRunner - .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [ - // No mock for 'devices', since it's running for macOS. - MockProcess(exitCode: 1), // 'drive' #1 - MockProcess(exitCode: 1), // 'drive' #2 - ]; - - Error? commandError; - final List output = - await runCapturingPrint(runner, ['drive-examples', '--macos'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('The following packages had errors:'), - contains(' plugin:\n' - ' example/integration_test/bar_test.dart\n' - ' example/integration_test/foo_test.dart'), - ]), - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'macos', - '--driver', - 'test_driver/integration_test.dart', - '--target', - 'integration_test/bar_test.dart', - ], - pluginExampleDirectory.path), - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'macos', - '--driver', - 'test_driver/integration_test.dart', - '--target', - 'integration_test/foo_test.dart', - ], - pluginExampleDirectory.path), - ])); - }); - - group('packages', () { - test('can be driven', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - 'example/test_driver/integration_test.dart', - 'example/web/index.html', - ]); - final Directory exampleDirectory = getExampleDir(package); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for a_package'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'web-server', - '--web-port=7357', - '--browser-name=chrome', - '--driver', - 'test_driver/integration_test.dart', - '--target', - 'integration_test/foo_test.dart' - ], - exampleDirectory.path), - ])); - }); - - test('are skipped when example does not support platform', () async { - createFakePackage('a_package', packagesDir, - isFlutter: true, - extraFiles: [ - 'example/integration_test/foo_test.dart', - 'example/test_driver/integration_test.dart', - ]); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for a_package'), - contains('Skipping a_package/example; does not support any ' - 'requested platforms'), - contains('SKIPPING: No example supports requested platform(s).'), - ]), - ); - - expect(processRunner.recordedCalls.isEmpty, true); - }); - - test('drive only supported examples if there is more than one', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - isFlutter: true, - examples: [ - 'with_web', - 'without_web' - ], - extraFiles: [ - 'example/with_web/integration_test/foo_test.dart', - 'example/with_web/test_driver/integration_test.dart', - 'example/with_web/web/index.html', - 'example/without_web/integration_test/foo_test.dart', - 'example/without_web/test_driver/integration_test.dart', - ]); - final Directory supportedExampleDirectory = - getExampleDir(package).childDirectory('with_web'); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for a_package'), - contains( - 'Skipping a_package/example/without_web; does not support any requested platforms.'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const [ - 'drive', - '-d', - 'web-server', - '--web-port=7357', - '--browser-name=chrome', - '--driver', - 'test_driver/integration_test.dart', - '--target', - 'integration_test/foo_test.dart' - ], - supportedExampleDirectory.path), - ])); - }); - - test('are skipped when there is no integration testing', () async { - createFakePackage('a_package', packagesDir, - isFlutter: true, extraFiles: ['example/web/index.html']); - - final List output = await runCapturingPrint(runner, [ - 'drive-examples', - '--web', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for a_package'), - contains('SKIPPING: No example is configured for driver tests.'), - ]), - ); - - expect(processRunner.recordedCalls.isEmpty, true); - }); - }); - }); -} diff --git a/script/tool/test/federation_safety_check_command_test.dart b/script/tool/test/federation_safety_check_command_test.dart deleted file mode 100644 index 6b6b1a514531..000000000000 --- a/script/tool/test/federation_safety_check_command_test.dart +++ /dev/null @@ -1,411 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/federation_safety_check_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void main() { - FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - - final MockGitDir gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - // Route git calls through the process runner, to make mock output - // consistent with other processes. Attach the first argument to the - // command to make targeting the mock results easier. - final String gitCommand = arguments.removeAt(0); - return processRunner.run('git-$gitCommand', arguments); - }); - - processRunner = RecordingProcessRunner(); - final FederationSafetyCheckCommand command = FederationSafetyCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: gitDir); - - runner = CommandRunner('federation_safety_check_command', - 'Test for $FederationSafetyCheckCommand'); - runner.addCommand(command); - }); - - test('skips non-plugin packages', () async { - final RepositoryPackage package = createFakePackage('foo', packagesDir); - - final String changedFileOutput = [ - package.libDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo...'), - contains('Not a plugin'), - contains('Skipped 1 package(s)'), - ]), - ); - }); - - test('skips unfederated plugins', () async { - final RepositoryPackage package = createFakePlugin('foo', packagesDir); - - final String changedFileOutput = [ - package.libDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo...'), - contains('Not a federated plugin'), - contains('Skipped 1 package(s)'), - ]), - ); - }); - - test('skips interface packages', () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage platformInterface = - createFakePlugin('foo_platform_interface', pluginGroupDir); - - final String changedFileOutput = [ - platformInterface.libDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo_platform_interface...'), - contains('Platform interface changes are not validated.'), - contains('Skipped 1 package(s)'), - ]), - ); - }); - - test('allows changes to just an interface package', () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage platformInterface = - createFakePlugin('foo_platform_interface', pluginGroupDir); - createFakePlugin('foo', pluginGroupDir); - createFakePlugin('foo_ios', pluginGroupDir); - createFakePlugin('foo_android', pluginGroupDir); - - final String changedFileOutput = [ - platformInterface.libDirectory.childFile('foo.dart'), - platformInterface.pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo/foo...'), - contains('No Dart changes.'), - contains('Running for foo_android...'), - contains('No Dart changes.'), - contains('Running for foo_ios...'), - contains('No Dart changes.'), - contains('Running for foo_platform_interface...'), - contains('Ran for 3 package(s)'), - contains('Skipped 1 package(s)'), - ]), - ); - expect( - output, - isNot(contains([ - contains('No published changes for foo_platform_interface'), - ])), - ); - }); - - test('allows changes to multiple non-interface packages', () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); - final RepositoryPackage implementation = - createFakePlugin('foo_bar', pluginGroupDir); - createFakePlugin('foo_platform_interface', pluginGroupDir); - - final String changedFileOutput = [ - appFacing.libDirectory.childFile('foo.dart'), - implementation.libDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo/foo...'), - contains('No published changes for foo_platform_interface.'), - contains('Running for foo_bar...'), - contains('No published changes for foo_platform_interface.'), - ]), - ); - }); - - test( - 'fails on changes to interface and non-interface packages in the same plugin', - () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); - final RepositoryPackage implementation = - createFakePlugin('foo_bar', pluginGroupDir); - final RepositoryPackage platformInterface = - createFakePlugin('foo_platform_interface', pluginGroupDir); - - final String changedFileOutput = [ - appFacing.libDirectory.childFile('foo.dart'), - implementation.libDirectory.childFile('foo.dart'), - platformInterface.pubspecFile, - platformInterface.libDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['federation-safety-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for foo/foo...'), - contains('Dart changes are not allowed to other packages in foo in the ' - 'same PR as changes to public Dart code in foo_platform_interface, ' - 'as this can cause accidental breaking changes to be missed by ' - 'automated checks. Please split the changes to these two packages ' - 'into separate PRs.'), - contains('Running for foo_bar...'), - contains('Dart changes are not allowed to other packages in foo'), - contains('The following packages had errors:'), - contains('foo/foo:\n' - ' foo_platform_interface changed.'), - contains('foo_bar:\n' - ' foo_platform_interface changed.'), - ]), - ); - }); - - test('ignores test-only changes to interface packages', () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); - final RepositoryPackage implementation = - createFakePlugin('foo_bar', pluginGroupDir); - final RepositoryPackage platformInterface = - createFakePlugin('foo_platform_interface', pluginGroupDir); - - final String changedFileOutput = [ - appFacing.libDirectory.childFile('foo.dart'), - implementation.libDirectory.childFile('foo.dart'), - platformInterface.pubspecFile, - platformInterface.testDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo/foo...'), - contains('No public code changes for foo_platform_interface.'), - contains('Running for foo_bar...'), - contains('No public code changes for foo_platform_interface.'), - ]), - ); - }); - - test('ignores unpublished changes to interface packages', () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); - final RepositoryPackage implementation = - createFakePlugin('foo_bar', pluginGroupDir); - final RepositoryPackage platformInterface = - createFakePlugin('foo_platform_interface', pluginGroupDir); - - final String changedFileOutput = [ - appFacing.libDirectory.childFile('foo.dart'), - implementation.libDirectory.childFile('foo.dart'), - platformInterface.pubspecFile, - platformInterface.libDirectory.childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - // Simulate no change to the version in the interface's pubspec.yaml. - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: platformInterface.pubspecFile.readAsStringSync()), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo/foo...'), - contains('No published changes for foo_platform_interface.'), - contains('Running for foo_bar...'), - contains('No published changes for foo_platform_interface.'), - ]), - ); - }); - - test('allows things that look like mass changes, with warning', () async { - final Directory pluginGroupDir = packagesDir.childDirectory('foo'); - final RepositoryPackage appFacing = createFakePlugin('foo', pluginGroupDir); - final RepositoryPackage implementation = - createFakePlugin('foo_bar', pluginGroupDir); - final RepositoryPackage platformInterface = - createFakePlugin('foo_platform_interface', pluginGroupDir); - - final RepositoryPackage otherPlugin1 = createFakePlugin('bar', packagesDir); - final RepositoryPackage otherPlugin2 = createFakePlugin('baz', packagesDir); - - final String changedFileOutput = [ - appFacing.libDirectory.childFile('foo.dart'), - implementation.libDirectory.childFile('foo.dart'), - platformInterface.pubspecFile, - platformInterface.libDirectory.childFile('foo.dart'), - otherPlugin1.libDirectory.childFile('bar.dart'), - otherPlugin2.libDirectory.childFile('baz.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo/foo...'), - contains( - 'Ignoring potentially dangerous change, as this appears to be a mass change.'), - contains('Running for foo_bar...'), - contains( - 'Ignoring potentially dangerous change, as this appears to be a mass change.'), - contains('Ran for 2 package(s) (2 with warnings)'), - ]), - ); - }); - - test('handles top-level files that match federated package heuristics', - () async { - final RepositoryPackage plugin = createFakePlugin('foo', packagesDir); - - final String changedFileOutput = [ - // This should be picked up as a change to 'foo', and not crash. - plugin.directory.childFile('foo_bar.baz'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for foo...'), - ]), - ); - }); - - test('handles deletion of an entire plugin', () async { - // Simulate deletion, in the form of diffs for packages that don't exist in - // the filesystem. - final String changedFileOutput = [ - packagesDir.childDirectory('foo').childFile('pubspec.yaml'), - packagesDir - .childDirectory('foo') - .childDirectory('lib') - .childFile('foo.dart'), - packagesDir - .childDirectory('foo_platform_interface') - .childFile('pubspec.yaml'), - packagesDir - .childDirectory('foo_platform_interface') - .childDirectory('lib') - .childFile('foo.dart'), - packagesDir.childDirectory('foo_web').childFile('pubspec.yaml'), - packagesDir - .childDirectory('foo_web') - .childDirectory('lib') - .childFile('foo.dart'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = - await runCapturingPrint(runner, ['federation-safety-check']); - - expect( - output, - containsAllInOrder([ - contains('Ran for 0 package(s)'), - ]), - ); - }); -} diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart deleted file mode 100644 index 68ea62b2334f..000000000000 --- a/script/tool/test/firebase_test_lab_command_test.dart +++ /dev/null @@ -1,795 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/file_utils.dart'; -import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('FirebaseTestLabCommand', () { - FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final FirebaseTestLabCommand command = FirebaseTestLabCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); - runner.addCommand(command); - }); - - void writeJavaTestFile(RepositoryPackage plugin, String relativeFilePath, - {String runnerClass = 'FlutterTestRunner'}) { - childFileWithSubcomponents( - plugin.directory, p.posix.split(relativeFilePath)) - .writeAsStringSync(''' -@DartIntegrationTest -@RunWith($runnerClass.class) -public class MainActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} -'''); - } - - test('fails if gcloud auth fails', () async { - processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess(exitCode: 1) - ]; - - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['firebase-test-lab'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to activate gcloud account.'), - ])); - }); - - test('retries gcloud set', () async { - processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess(), // auth - MockProcess(exitCode: 1), // config - ]; - - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - final List output = - await runCapturingPrint(runner, ['firebase-test-lab']); - - expect( - output, - containsAllInOrder([ - contains( - 'Warning: gcloud config set returned a non-zero exit code. Continuing anyway.'), - ])); - }); - - test('only runs gcloud configuration once', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir, extraFiles: [ - 'test/plugin_test.dart', - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin1, javaTestFileRelativePath); - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir, extraFiles: [ - 'test/plugin_test.dart', - 'example/integration_test/bar_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin2, javaTestFileRelativePath); - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin1'), - contains('Firebase project configured.'), - contains('Testing example/integration_test/foo_test.dart...'), - contains('Running for plugin2'), - contains('Testing example/integration_test/bar_test.dart...'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-cirrus'.split(' '), null), - ProcessCall( - '/packages/plugin1/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true'.split(' '), - '/packages/plugin1/example/android'), - ProcessCall( - '/packages/plugin1/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin1/example/integration_test/foo_test.dart' - .split(' '), - '/packages/plugin1/example/android'), - ProcessCall( - 'gcloud', - '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 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' - .split(' '), - '/packages/plugin1/example'), - ProcessCall( - '/packages/plugin2/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true'.split(' '), - '/packages/plugin2/example/android'), - ProcessCall( - '/packages/plugin2/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin2/example/integration_test/bar_test.dart' - .split(' '), - '/packages/plugin2/example/android'), - ProcessCall( - 'gcloud', - '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 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' - .split(' '), - '/packages/plugin2/example'), - ]), - ); - }); - - test('runs integration tests', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'test/plugin_test.dart', - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/integration_test/should_not_run.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Firebase project configured.'), - contains('Testing example/integration_test/bar_test.dart...'), - contains('Testing example/integration_test/foo_test.dart...'), - ]), - ); - expect(output, isNot(contains('test/plugin_test.dart'))); - expect(output, - isNot(contains('example/integration_test/should_not_run.dart'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-cirrus'.split(' '), null), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true'.split(' '), - '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/bar_test.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - '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 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - '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 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/1/ --device model=redfin,version=30 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example'), - ]), - ); - }); - - test('runs for all examples', () async { - const List examples = ['example1', 'example2']; - const String javaTestFileExampleRelativePath = - 'android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - examples: examples, - extraFiles: [ - for (final String example in examples) ...[ - 'example/$example/integration_test/a_test.dart', - 'example/$example/android/gradlew', - 'example/$example/$javaTestFileExampleRelativePath', - ], - ]); - for (final String example in examples) { - writeJavaTestFile( - plugin, 'example/$example/$javaTestFileExampleRelativePath'); - } - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - '--device', - 'model=seoul,version=26', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', - ]); - - expect( - output, - containsAllInOrder([ - contains('Testing example/example1/integration_test/a_test.dart...'), - contains('Testing example/example2/integration_test/a_test.dart...'), - ]), - ); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall( - '/packages/plugin/example/example1/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/example1/integration_test/a_test.dart' - .split(' '), - '/packages/plugin/example/example1/android'), - ProcessCall( - 'gcloud', - '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 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example1/0/ --device model=redfin,version=30 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example/example1'), - ProcessCall( - '/packages/plugin/example/example2/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/example2/integration_test/a_test.dart' - .split(' '), - '/packages/plugin/example/example2/android'), - ProcessCall( - 'gcloud', - '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 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example2/0/ --device model=redfin,version=30 --device model=seoul,version=26' - .split(' '), - '/packages/plugin/example/example2'), - ]), - ); - }); - - test('fails if a test fails twice', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess(), // auth - MockProcess(), // config - MockProcess(exitCode: 1), // integration test #1 - MockProcess(exitCode: 1), // integration test #1 retry - MockProcess(), // integration test #2 - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Testing example/integration_test/bar_test.dart...'), - contains('Testing example/integration_test/foo_test.dart...'), - contains('plugin:\n' - ' example/integration_test/bar_test.dart failed tests'), - ]), - ); - }); - - test('passes with warning if a test fails once, then passes on retry', - () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - processRunner.mockProcessesForExecutable['gcloud'] = [ - MockProcess(), // auth - MockProcess(), // config - MockProcess(exitCode: 1), // integration test #1 - MockProcess(), // integration test #1 retry - MockProcess(), // integration test #2 - ]; - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ]); - - expect( - output, - containsAllInOrder([ - contains('Testing example/integration_test/bar_test.dart...'), - contains('bar_test.dart failed on attempt 1. Retrying...'), - contains('Testing example/integration_test/foo_test.dart...'), - contains('Ran for 1 package(s) (1 with warnings)'), - ]), - ); - }); - - test('fails for packages with no androidTest directory', () async { - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - ]); - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No androidTest directory found.'), - contains('The following packages had errors:'), - contains('plugin:\n' - ' No tests ran (use --exclude if this is intentional).'), - ]), - ); - }); - - test('fails for packages with no integration test files', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No integration tests were run'), - contains('The following packages had errors:'), - contains('plugin:\n' - ' No tests ran (use --exclude if this is intentional).'), - ]), - ); - }); - - test('fails for packages with no integration_test runner', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'test/plugin_test.dart', - 'example/integration_test/bar_test.dart', - 'example/integration_test/foo_test.dart', - 'example/integration_test/should_not_run.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - // Use the wrong @RunWith annotation. - writeJavaTestFile(plugin, javaTestFileRelativePath, - runnerClass: 'AndroidJUnit4.class'); - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('No integration_test runner found. ' - 'See the integration_test package README for setup instructions.'), - contains('plugin:\n' - ' No integration_test runner.'), - ]), - ); - }); - - test('skips packages with no android directory', () async { - createFakePackage('package', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - ]); - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for package'), - contains('No examples support Android'), - ]), - ); - expect(output, - isNot(contains('Testing example/integration_test/foo_test.dart...'))); - - expect( - processRunner.recordedCalls, - orderedEquals([]), - ); - }); - - test('builds if gradlew is missing', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - final List output = await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Running flutter build apk...'), - contains('Firebase project configured.'), - contains('Testing example/integration_test/foo_test.dart...'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - 'build apk'.split(' '), - '/packages/plugin/example/android', - ), - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-cirrus'.split(' '), null), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true'.split(' '), - '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - '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 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' - .split(' '), - '/packages/plugin/example'), - ]), - ); - }); - - test('fails if building to generate gradlew fails', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(exitCode: 1) // flutter build - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to build example apk'), - ])); - }); - - test('fails if assembleAndroidTest fails', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to assemble androidTest'), - ])); - }); - - test('fails if assembleDebug fails', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(), // assembleAndroidTest - MockProcess(exitCode: 1), // assembleDebug - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Could not build example/integration_test/foo_test.dart'), - contains('The following packages had errors:'), - contains(' plugin:\n' - ' example/integration_test/foo_test.dart failed to build'), - ])); - }); - - test('experimental flag', () async { - const String javaTestFileRelativePath = - 'example/android/app/src/androidTest/MainActivityTest.java'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/integration_test/foo_test.dart', - 'example/android/gradlew', - javaTestFileRelativePath, - ]); - writeJavaTestFile(plugin, javaTestFileRelativePath); - - await runCapturingPrint(runner, [ - 'firebase-test-lab', - '--device', - 'model=redfin,version=30', - '--test-run-id', - 'testRunId', - '--build-id', - 'buildId', - '--enable-experiment=exp1', - ]); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'gcloud', - 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' - .split(' '), - null), - ProcessCall( - 'gcloud', 'config set project flutter-cirrus'.split(' '), null), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - '/packages/plugin/example/android/gradlew', - 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' - .split(' '), - '/packages/plugin/example/android'), - ProcessCall( - 'gcloud', - '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 7m --results-bucket=gs://flutter_cirrus_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/example/0/ --device model=redfin,version=30' - .split(' '), - '/packages/plugin/example'), - ]), - ); - }); - }); -} diff --git a/script/tool/test/fix_command_test.dart b/script/tool/test/fix_command_test.dart deleted file mode 100644 index 16061d2206cd..000000000000 --- a/script/tool/test/fix_command_test.dart +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/fix_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final FixCommand command = FixCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner('fix_command', 'Test for fix_command'); - runner.addCommand(command); - }); - - test('runs fix in top-level packages and subpackages', () async { - final RepositoryPackage package = createFakePackage('a', packagesDir); - final RepositoryPackage plugin = createFakePlugin('b', packagesDir); - - await runCapturingPrint(runner, ['fix']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('dart', const ['fix', '--apply'], package.path), - ProcessCall('dart', const ['fix', '--apply'], - package.getExamples().first.path), - ProcessCall('dart', const ['fix', '--apply'], plugin.path), - ProcessCall('dart', const ['fix', '--apply'], - plugin.getExamples().first.path), - ])); - }); - - test('fails if "dart fix" fails', () async { - createFakePlugin('foo', packagesDir); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, ['fix'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to automatically fix package.'), - ]), - ); - }); -} diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart deleted file mode 100644 index 634a996bccc6..000000000000 --- a/script/tool/test/format_command_test.dart +++ /dev/null @@ -1,663 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/file_utils.dart'; -import 'package:flutter_plugin_tools/src/format_command.dart'; -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - late FormatCommand analyzeCommand; - late CommandRunner runner; - late String javaFormatPath; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - analyzeCommand = FormatCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - // Create the java formatter file that the command checks for, to avoid - // a download. - final p.Context path = analyzeCommand.path; - javaFormatPath = path.join(path.dirname(path.fromUri(mockPlatform.script)), - 'google-java-format-1.3-all-deps.jar'); - fileSystem.file(javaFormatPath).createSync(recursive: true); - - runner = CommandRunner('format_command', 'Test for format_command'); - runner.addCommand(analyzeCommand); - }); - - /// Returns a modified version of a list of [relativePaths] that are relative - /// to [package] to instead be relative to [packagesDir]. - List getPackagesDirRelativePaths( - RepositoryPackage package, List relativePaths) { - final p.Context path = analyzeCommand.path; - final String relativeBase = - path.relative(package.path, from: packagesDir.path); - return relativePaths - .map((String relativePath) => path.join(relativeBase, relativePath)) - .toList(); - } - - /// Returns a list of [count] relative paths to pass to [createFakePlugin] - /// or [createFakePackage] with name [packageName] such that each path will - /// be 99 characters long relative to [packagesDir]. - /// - /// This is for each of testing batching, since it means each file will - /// consume 100 characters of the batch length. - List get99CharacterPathExtraFiles(String packageName, int count) { - final int padding = 99 - - packageName.length - - 1 - // the path separator after the package name - 1 - // the path separator after the padding - 10; // the file name - const int filenameBase = 10000; - - final p.Context path = analyzeCommand.path; - return [ - for (int i = filenameBase; i < filenameBase + count; ++i) - path.join('a' * padding, '$i.dart'), - ]; - } - - test('formats .dart files', () async { - const List files = [ - 'lib/a.dart', - 'lib/src/b.dart', - 'lib/src/c.dart', - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: files, - ); - - await runCapturingPrint(runner, ['format']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'dart', - ['format', ...getPackagesDirRelativePaths(plugin, files)], - packagesDir.path), - ])); - }); - - test('does not format .dart files with pragma', () async { - const List formattedFiles = [ - 'lib/a.dart', - 'lib/src/b.dart', - 'lib/src/c.dart', - ]; - const String unformattedFile = 'lib/src/d.dart'; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: [ - ...formattedFiles, - unformattedFile, - ], - ); - - final p.Context posixContext = p.posix; - childFileWithSubcomponents( - plugin.directory, posixContext.split(unformattedFile)) - .writeAsStringSync( - '// copyright bla bla\n// This file is hand-formatted.\ncode...'); - - await runCapturingPrint(runner, ['format']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'dart', - [ - 'format', - ...getPackagesDirRelativePaths(plugin, formattedFiles) - ], - packagesDir.path), - ])); - }); - - test('fails if dart format fails', () async { - const List files = [ - 'lib/a.dart', - 'lib/src/b.dart', - 'lib/src/c.dart', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1) - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['format'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Failed to format Dart files: exit code 1.'), - ])); - }); - - test('formats .java files', () async { - const List files = [ - 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', - 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: files, - ); - - await runCapturingPrint(runner, ['format']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall('java', ['-version'], null), - ProcessCall( - 'java', - [ - '-jar', - javaFormatPath, - '--replace', - ...getPackagesDirRelativePaths(plugin, files) - ], - packagesDir.path), - ])); - }); - - test('fails with a clear message if Java is not in the path', () async { - const List files = [ - 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', - 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - processRunner.mockProcessesForExecutable['java'] = [ - MockProcess(exitCode: 1) - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['format'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Unable to run "java". Make sure that it is in your path, or ' - 'provide a full path with --java.'), - ])); - }); - - test('fails if Java formatter fails', () async { - const List files = [ - 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', - 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - processRunner.mockProcessesForExecutable['java'] = [ - MockProcess(), // check for working java - MockProcess(exitCode: 1), // format - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['format'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Failed to format Java files: exit code 1.'), - ])); - }); - - test('honors --java flag', () async { - const List files = [ - 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', - 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: files, - ); - - await runCapturingPrint(runner, ['format', '--java=/path/to/java']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall('/path/to/java', ['--version'], null), - ProcessCall( - '/path/to/java', - [ - '-jar', - javaFormatPath, - '--replace', - ...getPackagesDirRelativePaths(plugin, files) - ], - packagesDir.path), - ])); - }); - - test('formats c-ish files', () async { - const List files = [ - 'ios/Classes/Foo.h', - 'ios/Classes/Foo.m', - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - 'macos/Classes/Foo.mm', - 'windows/foo_plugin.cpp', - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: files, - ); - - await runCapturingPrint(runner, ['format']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall('clang-format', ['--version'], null), - ProcessCall( - 'clang-format', - [ - '-i', - '--style=file', - ...getPackagesDirRelativePaths(plugin, files) - ], - packagesDir.path), - ])); - }); - - test('fails with a clear message if clang-format is not in the path', - () async { - const List files = [ - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - processRunner.mockProcessesForExecutable['clang-format'] = [ - MockProcess(exitCode: 1) - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['format'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to run "clang-format". Make sure that it is in your ' - 'path, or provide a full path with --clang-format.'), - ])); - }); - - test('falls back to working clang-format in the path', () async { - const List files = [ - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: files, - ); - - processRunner.mockProcessesForExecutable['clang-format'] = [ - MockProcess(exitCode: 1) - ]; - processRunner.mockProcessesForExecutable['which'] = [ - MockProcess( - stdout: '/usr/local/bin/clang-format\n/path/to/working-clang-format') - ]; - processRunner.mockProcessesForExecutable['/usr/local/bin/clang-format'] = - [MockProcess(exitCode: 1)]; - await runCapturingPrint(runner, ['format']); - - expect( - processRunner.recordedCalls, - containsAll([ - const ProcessCall( - '/path/to/working-clang-format', ['--version'], null), - ProcessCall( - '/path/to/working-clang-format', - [ - '-i', - '--style=file', - ...getPackagesDirRelativePaths(plugin, files) - ], - packagesDir.path), - ])); - }); - - test('honors --clang-format flag', () async { - const List files = [ - 'windows/foo_plugin.cpp', - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: files, - ); - - await runCapturingPrint( - runner, ['format', '--clang-format=/path/to/clang-format']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall( - '/path/to/clang-format', ['--version'], null), - ProcessCall( - '/path/to/clang-format', - [ - '-i', - '--style=file', - ...getPackagesDirRelativePaths(plugin, files) - ], - packagesDir.path), - ])); - }); - - test('fails if clang-format fails', () async { - const List files = [ - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - processRunner.mockProcessesForExecutable['clang-format'] = [ - MockProcess(), // check for working clang-format - MockProcess(exitCode: 1), // format - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['format'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Failed to format C, C++, and Objective-C files: exit code 1.'), - ])); - }); - - test('skips known non-repo files', () async { - const List skipFiles = [ - '/example/build/SomeFramework.framework/Headers/SomeFramework.h', - '/example/Pods/APod.framework/Headers/APod.h', - '.dart_tool/internals/foo.cc', - '.dart_tool/internals/Bar.java', - '.dart_tool/internals/baz.dart', - ]; - const List clangFiles = ['ios/Classes/Foo.h']; - const List dartFiles = ['lib/a.dart']; - const List javaFiles = [ - 'android/src/main/java/io/flutter/plugins/a_plugin/a.java' - ]; - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - extraFiles: [ - ...skipFiles, - // Include some files that should be formatted to validate that it's - // correctly filtering even when running the commands. - ...clangFiles, - ...dartFiles, - ...javaFiles, - ], - ); - - await runCapturingPrint(runner, ['format']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall( - 'clang-format', - [ - '-i', - '--style=file', - ...getPackagesDirRelativePaths(plugin, clangFiles) - ], - packagesDir.path), - ProcessCall( - 'dart', - [ - 'format', - ...getPackagesDirRelativePaths(plugin, dartFiles) - ], - packagesDir.path), - ProcessCall( - 'java', - [ - '-jar', - javaFormatPath, - '--replace', - ...getPackagesDirRelativePaths(plugin, javaFiles) - ], - packagesDir.path), - ])); - }); - - test('fails if files are changed with --fail-on-change', () async { - const List files = [ - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; - processRunner.mockProcessesForExecutable['git'] = [ - MockProcess(stdout: changedFilePath), - ]; - - Error? commandError; - final List output = - await runCapturingPrint(runner, ['format', '--fail-on-change'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('These files are not formatted correctly'), - contains(changedFilePath), - contains('patch -p1 < files = [ - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - processRunner.mockProcessesForExecutable['git'] = [ - MockProcess(exitCode: 1) - ]; - Error? commandError; - final List output = - await runCapturingPrint(runner, ['format', '--fail-on-change'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to determine changed files.'), - ])); - }); - - test('reports git diff failures', () async { - const List files = [ - 'linux/foo_plugin.cc', - 'macos/Classes/Foo.h', - ]; - createFakePlugin('a_plugin', packagesDir, extraFiles: files); - - const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; - processRunner.mockProcessesForExecutable['git'] = [ - MockProcess(stdout: changedFilePath), // ls-files - MockProcess(exitCode: 1), // diff - ]; - - Error? commandError; - final List output = - await runCapturingPrint(runner, ['format', '--fail-on-change'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('These files are not formatted correctly'), - contains(changedFilePath), - contains('Unable to determine diff.'), - ])); - }); - - test('Batches moderately long file lists on Windows', () async { - mockPlatform.isWindows = true; - - const String pluginName = 'a_plugin'; - // -1 since the command itself takes some length. - const int batchSize = (windowsCommandLineMax ~/ 100) - 1; - - // Make the file list one file longer than would fit in the batch. - final List batch1 = - get99CharacterPathExtraFiles(pluginName, batchSize + 1); - final String extraFile = batch1.removeLast(); - - createFakePlugin( - pluginName, - packagesDir, - extraFiles: [...batch1, extraFile], - ); - - await runCapturingPrint(runner, ['format']); - - // Ensure that it was batched... - expect(processRunner.recordedCalls.length, 2); - // ... and that the spillover into the second batch was only one file. - expect( - processRunner.recordedCalls, - contains( - ProcessCall( - 'dart', - [ - 'format', - '$pluginName\\$extraFile', - ], - packagesDir.path), - )); - }); - - // Validates that the Windows limit--which is much lower than the limit on - // other platforms--isn't being used on all platforms, as that would make - // formatting slower on Linux and macOS. - test('Does not batch moderately long file lists on non-Windows', () async { - const String pluginName = 'a_plugin'; - // -1 since the command itself takes some length. - const int batchSize = (windowsCommandLineMax ~/ 100) - 1; - - // Make the file list one file longer than would fit in a Windows batch. - final List batch = - get99CharacterPathExtraFiles(pluginName, batchSize + 1); - - createFakePlugin( - pluginName, - packagesDir, - extraFiles: batch, - ); - - await runCapturingPrint(runner, ['format']); - - expect(processRunner.recordedCalls.length, 1); - }); - - test('Batches extremely long file lists on non-Windows', () async { - const String pluginName = 'a_plugin'; - // -1 since the command itself takes some length. - const int batchSize = (nonWindowsCommandLineMax ~/ 100) - 1; - - // Make the file list one file longer than would fit in the batch. - final List batch1 = - get99CharacterPathExtraFiles(pluginName, batchSize + 1); - final String extraFile = batch1.removeLast(); - - createFakePlugin( - pluginName, - packagesDir, - extraFiles: [...batch1, extraFile], - ); - - await runCapturingPrint(runner, ['format']); - - // Ensure that it was batched... - expect(processRunner.recordedCalls.length, 2); - // ... and that the spillover into the second batch was only one file. - expect( - processRunner.recordedCalls, - contains( - ProcessCall( - 'dart', - [ - 'format', - '$pluginName/$extraFile', - ], - packagesDir.path), - )); - }); -} diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart deleted file mode 100644 index 09841df74e70..000000000000 --- a/script/tool/test/license_check_command_test.dart +++ /dev/null @@ -1,613 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/license_check_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('LicenseCheckCommand', () { - late CommandRunner runner; - late FileSystem fileSystem; - late Platform platform; - late Directory root; - - setUp(() { - fileSystem = MemoryFileSystem(); - platform = MockPlatformWithSeparator(); - final Directory packagesDir = - fileSystem.currentDirectory.childDirectory('packages'); - root = packagesDir.parent; - - final MockGitDir gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - - final LicenseCheckCommand command = LicenseCheckCommand( - packagesDir, - platform: platform, - gitDir: gitDir, - ); - runner = - CommandRunner('license_test', 'Test for $LicenseCheckCommand'); - runner.addCommand(command); - }); - - /// Writes a copyright+license block to [file], defaulting to a standard - /// block for this repository. - /// - /// [commentString] is added to the start of each line. - /// [prefix] is added to the start of the entire block. - /// [suffix] is added to the end of the entire block. - void writeLicense( - File file, { - String comment = '// ', - String prefix = '', - String suffix = '', - String copyright = - 'Copyright 2013 The Flutter Authors. All rights reserved.', - List license = const [ - 'Use of this source code is governed by a BSD-style license that can be', - 'found in the LICENSE file.', - ], - bool useCrlf = false, - }) { - final List lines = ['$prefix$comment$copyright']; - for (final String line in license) { - lines.add('$comment$line'); - } - final String newline = useCrlf ? '\r\n' : '\n'; - file.writeAsStringSync(lines.join(newline) + suffix + newline); - } - - test('looks at only expected extensions', () async { - final Map extensions = { - 'c': true, - 'cc': true, - 'cpp': true, - 'dart': true, - 'h': true, - 'html': true, - 'java': true, - 'json': false, - 'kt': true, - 'm': true, - 'md': false, - 'mm': true, - 'png': false, - 'swift': true, - 'sh': true, - 'yaml': false, - }; - - const String filenameBase = 'a_file'; - for (final String fileExtension in extensions.keys) { - root.childFile('$filenameBase.$fileExtension').createSync(); - } - - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - // Ignore failure; the files are empty so the check is expected to fail, - // but this test isn't for that behavior. - }); - - extensions.forEach((String fileExtension, bool shouldCheck) { - final Matcher logLineMatcher = - contains('Checking $filenameBase.$fileExtension'); - expect(output, shouldCheck ? logLineMatcher : isNot(logLineMatcher)); - }); - }); - - test('ignore list overrides extension matches', () async { - final List ignoredFiles = [ - // Ignored base names. - 'flutter_export_environment.sh', - 'GeneratedPluginRegistrant.java', - 'GeneratedPluginRegistrant.m', - 'generated_plugin_registrant.cc', - 'generated_plugin_registrant.cpp', - // Ignored path suffixes. - 'foo.g.dart', - 'foo.mocks.dart', - // Ignored files. - 'resource.h', - ]; - - for (final String name in ignoredFiles) { - root.childFile(name).createSync(); - } - - final List output = - await runCapturingPrint(runner, ['license-check']); - - for (final String name in ignoredFiles) { - expect(output, isNot(contains('Checking $name'))); - } - }); - - test('ignores submodules', () async { - const String submoduleName = 'a_submodule'; - - final File submoduleSpec = root.childFile('.gitmodules'); - submoduleSpec.writeAsStringSync(''' -[submodule "$submoduleName"] - path = $submoduleName - url = https://github.com/foo/$submoduleName -'''); - - const List submoduleFiles = [ - '$submoduleName/foo.dart', - '$submoduleName/a/b/bar.dart', - '$submoduleName/LICENSE', - ]; - for (final String filePath in submoduleFiles) { - root.childFile(filePath).createSync(recursive: true); - } - - final List output = - await runCapturingPrint(runner, ['license-check']); - - for (final String filePath in submoduleFiles) { - expect(output, isNot(contains('Checking $filePath'))); - } - }); - - test('passes if all checked files have license blocks', () async { - final File checked = root.childFile('checked.cc'); - checked.createSync(); - writeLicense(checked); - final File notChecked = root.childFile('not_checked.md'); - notChecked.createSync(); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check a file. - expect( - output, - containsAllInOrder([ - contains('Checking checked.cc'), - contains('All files passed validation!'), - ])); - }); - - test('passes correct license blocks on Windows', () async { - final File checked = root.childFile('checked.cc'); - checked.createSync(); - writeLicense(checked, useCrlf: true); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check a file. - expect( - output, - containsAllInOrder([ - contains('Checking checked.cc'), - contains('All files passed validation!'), - ])); - }); - - test('handles the comment styles for all supported languages', () async { - final File fileA = root.childFile('file_a.cc'); - fileA.createSync(); - writeLicense(fileA); - final File fileB = root.childFile('file_b.sh'); - fileB.createSync(); - writeLicense(fileB, comment: '# '); - final File fileC = root.childFile('file_c.html'); - fileC.createSync(); - writeLicense(fileC, comment: '', prefix: ''); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check the files. - expect( - output, - containsAllInOrder([ - contains('Checking file_a.cc'), - contains('Checking file_b.sh'), - contains('Checking file_c.html'), - contains('All files passed validation!'), - ])); - }); - - test('fails if any checked files are missing license blocks', () async { - final File goodA = root.childFile('good.cc'); - goodA.createSync(); - writeLicense(goodA); - final File goodB = root.childFile('good.h'); - goodB.createSync(); - writeLicense(goodB); - root.childFile('bad.cc').createSync(); - root.childFile('bad.h').createSync(); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - // Failure should give information about the problematic files. - expect( - output, - containsAllInOrder([ - contains( - 'The license block for these files is missing or incorrect:'), - contains(' bad.cc'), - contains(' bad.h'), - ])); - // Failure shouldn't print the success message. - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('fails if any checked files are missing just the copyright', () async { - final File good = root.childFile('good.cc'); - good.createSync(); - writeLicense(good); - final File bad = root.childFile('bad.cc'); - bad.createSync(); - writeLicense(bad, copyright: ''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - // Failure should give information about the problematic files. - expect( - output, - containsAllInOrder([ - contains( - 'The license block for these files is missing or incorrect:'), - contains(' bad.cc'), - ])); - // Failure shouldn't print the success message. - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('fails if any checked files are missing just the license', () async { - final File good = root.childFile('good.cc'); - good.createSync(); - writeLicense(good); - final File bad = root.childFile('bad.cc'); - bad.createSync(); - writeLicense(bad, license: []); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - // Failure should give information about the problematic files. - expect( - output, - containsAllInOrder([ - contains( - 'The license block for these files is missing or incorrect:'), - contains(' bad.cc'), - ])); - // Failure shouldn't print the success message. - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('fails if any third-party code is not in a third_party directory', - () async { - final File thirdPartyFile = root.childFile('third_party.cc'); - thirdPartyFile.createSync(); - writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - // Failure should give information about the problematic files. - expect( - output, - containsAllInOrder([ - contains( - 'The license block for these files is missing or incorrect:'), - contains(' third_party.cc'), - ])); - // Failure shouldn't print the success message. - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('succeeds for third-party code in a third_party directory', () async { - final File thirdPartyFile = root - .childDirectory('a_plugin') - .childDirectory('lib') - .childDirectory('src') - .childDirectory('third_party') - .childFile('file.cc'); - thirdPartyFile.createSync(recursive: true); - writeLicense(thirdPartyFile, - copyright: 'Copyright 2017 Workiva Inc.', - license: [ - 'Licensed under the Apache License, Version 2.0 (the "License");', - 'you may not use this file except in compliance with the License.' - ]); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check the file. - expect( - output, - containsAllInOrder([ - contains('Checking a_plugin/lib/src/third_party/file.cc'), - contains('All files passed validation!'), - ])); - }); - - test('allows first-party code in a third_party directory', () async { - final File firstPartyFileInThirdParty = root - .childDirectory('a_plugin') - .childDirectory('lib') - .childDirectory('src') - .childDirectory('third_party') - .childFile('first_party.cc'); - firstPartyFileInThirdParty.createSync(recursive: true); - writeLicense(firstPartyFileInThirdParty); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check the file. - expect( - output, - containsAllInOrder([ - contains('Checking a_plugin/lib/src/third_party/first_party.cc'), - contains('All files passed validation!'), - ])); - }); - - test('fails for licenses that the tool does not expect', () async { - final File good = root.childFile('good.cc'); - good.createSync(); - writeLicense(good); - final File bad = root.childDirectory('third_party').childFile('bad.cc'); - bad.createSync(recursive: true); - writeLicense(bad, license: [ - 'This program is free software: you can redistribute it and/or modify', - 'it under the terms of the GNU General Public License', - ]); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - // Failure should give information about the problematic files. - expect( - output, - containsAllInOrder([ - contains( - 'No recognized license was found for the following third-party files:'), - contains(' third_party/bad.cc'), - ])); - // Failure shouldn't print the success message. - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('Apache is not recognized for new authors without validation changes', - () async { - final File good = root.childFile('good.cc'); - good.createSync(); - writeLicense(good); - final File bad = root.childDirectory('third_party').childFile('bad.cc'); - bad.createSync(recursive: true); - writeLicense( - bad, - copyright: 'Copyright 2017 Some New Authors.', - license: [ - 'Licensed under the Apache License, Version 2.0 (the "License");', - 'you may not use this file except in compliance with the License.' - ], - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - // Failure should give information about the problematic files. - expect( - output, - containsAllInOrder([ - contains( - 'No recognized license was found for the following third-party files:'), - contains(' third_party/bad.cc'), - ])); - // Failure shouldn't print the success message. - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('passes if all first-party LICENSE files are correctly formatted', - () async { - final File license = root.childFile('LICENSE'); - license.createSync(); - license.writeAsStringSync(_correctLicenseFileText); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check the file. - expect( - output, - containsAllInOrder([ - contains('Checking LICENSE'), - contains('All files passed validation!'), - ])); - }); - - test('passes correct LICENSE files on Windows', () async { - final File license = root.childFile('LICENSE'); - license.createSync(); - license - .writeAsStringSync(_correctLicenseFileText.replaceAll('\n', '\r\n')); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // Sanity check that the test did actually check the file. - expect( - output, - containsAllInOrder([ - contains('Checking LICENSE'), - contains('All files passed validation!'), - ])); - }); - - test('fails if any first-party LICENSE files are incorrectly formatted', - () async { - final File license = root.childFile('LICENSE'); - license.createSync(); - license.writeAsStringSync(_incorrectLicenseFileText); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect(output, isNot(contains(contains('All files passed validation!')))); - }); - - test('ignores third-party LICENSE format', () async { - final File license = - root.childDirectory('third_party').childFile('LICENSE'); - license.createSync(recursive: true); - license.writeAsStringSync(_incorrectLicenseFileText); - - final List output = - await runCapturingPrint(runner, ['license-check']); - - // The file shouldn't be checked. - expect(output, isNot(contains(contains('Checking third_party/LICENSE')))); - }); - - test('outputs all errors at the end', () async { - root.childFile('bad.cc').createSync(); - root - .childDirectory('third_party') - .childFile('bad.cc') - .createSync(recursive: true); - final File license = root.childFile('LICENSE'); - license.createSync(); - license.writeAsStringSync(_incorrectLicenseFileText); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['license-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Checking LICENSE'), - contains('Checking bad.cc'), - contains('Checking third_party/bad.cc'), - contains( - 'The following LICENSE files do not follow the expected format:'), - contains(' LICENSE'), - contains( - 'The license block for these files is missing or incorrect:'), - contains(' bad.cc'), - contains( - 'No recognized license was found for the following third-party files:'), - contains(' third_party/bad.cc'), - ])); - }); - }); -} - -class MockPlatformWithSeparator extends MockPlatform { - @override - String get pathSeparator => isWindows ? r'\' : '/'; -} - -const String _correctLicenseFileText = ''' -Copyright 2013 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -'''; - -// A common incorrect version created by copying text intended for a code file, -// with comment markers. -const String _incorrectLicenseFileText = ''' -// Copyright 2013 The Flutter Authors. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -'''; diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart deleted file mode 100644 index e4a6c5c859e4..000000000000 --- a/script/tool/test/lint_android_command_test.dart +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/lint_android_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('LintAndroidCommand', () { - FileSystem fileSystem; - late Directory packagesDir; - late CommandRunner runner; - late MockPlatform mockPlatform; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - mockPlatform = MockPlatform(); - processRunner = RecordingProcessRunner(); - final LintAndroidCommand command = LintAndroidCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'lint_android_test', 'Test for $LintAndroidCommand'); - runner.addCommand(command); - }); - - test('runs gradle lint', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin1', packagesDir, extraFiles: [ - 'example/android/gradlew', - ], platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }); - - final Directory androidDir = - plugin.getExamples().first.platformDirectory(FlutterPlatform.android); - - final List output = - await runCapturingPrint(runner, ['lint-android']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidDir.childFile('gradlew').path, - const ['plugin1:lintDebug'], - androidDir.path, - ), - ]), - ); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin1'), - contains('No issues found!'), - ])); - }); - - test('runs on all examples', () async { - final List examples = ['example1', 'example2']; - final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, - examples: examples, - extraFiles: [ - 'example/example1/android/gradlew', - 'example/example2/android/gradlew', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }); - - final Iterable exampleAndroidDirs = plugin.getExamples().map( - (RepositoryPackage example) => - example.platformDirectory(FlutterPlatform.android)); - - final List output = - await runCapturingPrint(runner, ['lint-android']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - for (final Directory directory in exampleAndroidDirs) - ProcessCall( - directory.childFile('gradlew').path, - const ['plugin1:lintDebug'], - directory.path, - ), - ]), - ); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin1'), - contains('No issues found!'), - ])); - }); - - test('fails if gradlew is missing', () async { - createFakePlugin('plugin1', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['lint-android'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder( - [ - contains('Build examples before linting'), - ], - )); - }); - - test('fails if linting finds issues', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin1', packagesDir, extraFiles: [ - 'example/android/gradlew', - ], platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }); - - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['lint-android'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder( - [ - contains('The following packages had errors:'), - ], - )); - }); - - test('skips non-Android plugins', () async { - createFakePlugin('plugin1', packagesDir); - - final List output = - await runCapturingPrint(runner, ['lint-android']); - - expect( - output, - containsAllInOrder( - [ - contains( - 'SKIPPING: Plugin does not have an Android implementation.') - ], - )); - }); - - test('skips non-inline plugins', () async { - createFakePlugin('plugin1', packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.federated) - }); - - final List output = - await runCapturingPrint(runner, ['lint-android']); - - expect( - output, - containsAllInOrder( - [ - contains( - 'SKIPPING: Plugin does not have an Android implementation.') - ], - )); - }); - }); -} diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart deleted file mode 100644 index f19215c89b9e..000000000000 --- a/script/tool/test/list_command_test.dart +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/list_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('ListCommand', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - final ListCommand command = - ListCommand(packagesDir, platform: mockPlatform); - - runner = CommandRunner('list_test', 'Test for $ListCommand'); - runner.addCommand(command); - }); - - test('lists top-level packages', () async { - createFakePackage('package1', packagesDir); - createFakePlugin('plugin2', packagesDir); - - final List plugins = - await runCapturingPrint(runner, ['list', '--type=package']); - - expect( - plugins, - orderedEquals([ - '/packages/package1', - '/packages/plugin2', - ]), - ); - }); - - test('lists examples', () async { - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir, - examples: ['example1', 'example2']); - createFakePlugin('plugin3', packagesDir, examples: []); - - final List examples = - await runCapturingPrint(runner, ['list', '--type=example']); - - expect( - examples, - orderedEquals([ - '/packages/plugin1/example', - '/packages/plugin2/example/example1', - '/packages/plugin2/example/example2', - ]), - ); - }); - - test('lists packages and subpackages', () async { - createFakePackage('package1', packagesDir); - createFakePlugin('plugin2', packagesDir, - examples: ['example1', 'example2']); - createFakePlugin('plugin3', packagesDir, examples: []); - - final List packages = await runCapturingPrint( - runner, ['list', '--type=package-or-subpackage']); - - expect( - packages, - unorderedEquals([ - '/packages/package1', - '/packages/package1/example', - '/packages/plugin2', - '/packages/plugin2/example/example1', - '/packages/plugin2/example/example2', - '/packages/plugin3', - ]), - ); - }); - - test('lists files', () async { - createFakePlugin('plugin1', packagesDir); - createFakePlugin('plugin2', packagesDir, - examples: ['example1', 'example2']); - createFakePlugin('plugin3', packagesDir, examples: []); - - final List examples = - await runCapturingPrint(runner, ['list', '--type=file']); - - expect( - examples, - unorderedEquals([ - '/packages/plugin1/pubspec.yaml', - '/packages/plugin1/AUTHORS', - '/packages/plugin1/CHANGELOG.md', - '/packages/plugin1/README.md', - '/packages/plugin1/example/pubspec.yaml', - '/packages/plugin2/pubspec.yaml', - '/packages/plugin2/AUTHORS', - '/packages/plugin2/CHANGELOG.md', - '/packages/plugin2/README.md', - '/packages/plugin2/example/example1/pubspec.yaml', - '/packages/plugin2/example/example2/pubspec.yaml', - '/packages/plugin3/pubspec.yaml', - '/packages/plugin3/AUTHORS', - '/packages/plugin3/CHANGELOG.md', - '/packages/plugin3/README.md', - ]), - ); - }); - - test('lists plugins using federated plugin layout', () async { - createFakePlugin('plugin1', packagesDir); - - // Create a federated plugin by creating a directory under the packages - // directory with several packages underneath. - final Directory federatedPluginDir = - packagesDir.childDirectory('my_plugin')..createSync(); - createFakePlugin('my_plugin', federatedPluginDir); - createFakePlugin('my_plugin_web', federatedPluginDir); - createFakePlugin('my_plugin_macos', federatedPluginDir); - - // Test without specifying `--type`. - final List plugins = - await runCapturingPrint(runner, ['list']); - - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - '/packages/my_plugin/my_plugin', - '/packages/my_plugin/my_plugin_web', - '/packages/my_plugin/my_plugin_macos', - ]), - ); - }); - - test('can filter plugins with the --packages argument', () async { - createFakePlugin('plugin1', packagesDir); - - // Create a federated plugin by creating a directory under the packages - // directory with several packages underneath. - final Directory federatedPluginDir = - packagesDir.childDirectory('my_plugin')..createSync(); - createFakePlugin('my_plugin', federatedPluginDir); - createFakePlugin('my_plugin_web', federatedPluginDir); - createFakePlugin('my_plugin_macos', federatedPluginDir); - - List plugins = await runCapturingPrint( - runner, ['list', '--packages=plugin1']); - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - ]), - ); - - plugins = await runCapturingPrint( - runner, ['list', '--packages=my_plugin']); - expect( - plugins, - unorderedEquals([ - '/packages/my_plugin/my_plugin', - '/packages/my_plugin/my_plugin_web', - '/packages/my_plugin/my_plugin_macos', - ]), - ); - - plugins = await runCapturingPrint( - runner, ['list', '--packages=my_plugin/my_plugin_web']); - expect( - plugins, - unorderedEquals([ - '/packages/my_plugin/my_plugin_web', - ]), - ); - - plugins = await runCapturingPrint(runner, - ['list', '--packages=my_plugin/my_plugin_web,plugin1']); - expect( - plugins, - unorderedEquals([ - '/packages/plugin1', - '/packages/my_plugin/my_plugin_web', - ]), - ); - }); - }); -} diff --git a/script/tool/test/make_deps_path_based_command_test.dart b/script/tool/test/make_deps_path_based_command_test.dart deleted file mode 100644 index e846a63fc68e..000000000000 --- a/script/tool/test/make_deps_path_based_command_test.dart +++ /dev/null @@ -1,483 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/make_deps_path_based_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void main() { - FileSystem fileSystem; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - - final MockGitDir gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - // Route git calls through the process runner, to make mock output - // consistent with other processes. Attach the first argument to the - // command to make targeting the mock results easier. - final String gitCommand = arguments.removeAt(0); - return processRunner.run('git-$gitCommand', arguments); - }); - - processRunner = RecordingProcessRunner(); - final MakeDepsPathBasedCommand command = - MakeDepsPathBasedCommand(packagesDir, gitDir: gitDir); - - runner = CommandRunner( - 'make-deps-path-based_command', 'Test for $MakeDepsPathBasedCommand'); - runner.addCommand(command); - }); - - /// Adds dummy 'dependencies:' entries for each package in [dependencies] - /// to [package]. - void addDependencies( - RepositoryPackage package, Iterable dependencies) { - final List lines = package.pubspecFile.readAsLinesSync(); - final int dependenciesStartIndex = lines.indexOf('dependencies:'); - assert(dependenciesStartIndex != -1); - lines.insertAll(dependenciesStartIndex + 1, [ - for (final String dependency in dependencies) ' $dependency: ^1.0.0', - ]); - package.pubspecFile.writeAsStringSync(lines.join('\n')); - } - - /// Adds a 'dev_dependencies:' section with entries for each package in - /// [dependencies] to [package]. - void addDevDependenciesSection( - RepositoryPackage package, Iterable devDependencies) { - final String originalContent = package.pubspecFile.readAsStringSync(); - package.pubspecFile.writeAsStringSync(''' -$originalContent - -dev_dependencies: -${devDependencies.map((String dep) => ' $dep: ^1.0.0').join('\n')} -'''); - } - - test('no-ops for no plugins', () async { - createFakePackage('foo', packagesDir, isFlutter: true); - final RepositoryPackage packageBar = - createFakePackage('bar', packagesDir, isFlutter: true); - addDependencies(packageBar, ['foo']); - final String originalPubspecContents = - packageBar.pubspecFile.readAsStringSync(); - - final List output = - await runCapturingPrint(runner, ['make-deps-path-based']); - - expect( - output, - containsAllInOrder([ - contains('No target dependencies'), - ]), - ); - // The 'foo' reference should not have been modified. - expect(packageBar.pubspecFile.readAsStringSync(), originalPubspecContents); - }); - - test('rewrites "dependencies" references', () async { - final RepositoryPackage simplePackage = - createFakePackage('foo', packagesDir, isFlutter: true); - final Directory pluginGroup = packagesDir.childDirectory('bar'); - - createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); - final RepositoryPackage pluginImplementation = - createFakePlugin('bar_android', pluginGroup); - final RepositoryPackage pluginAppFacing = - createFakePlugin('bar', pluginGroup); - - addDependencies(simplePackage, [ - 'bar', - 'bar_android', - 'bar_platform_interface', - ]); - addDependencies(pluginAppFacing, [ - 'bar_platform_interface', - 'bar_android', - ]); - addDependencies(pluginImplementation, [ - 'bar_platform_interface', - ]); - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies=bar,bar_platform_interface' - ]); - - expect( - output, - containsAll([ - 'Rewriting references to: bar, bar_platform_interface...', - ' Modified packages/bar/bar/pubspec.yaml', - ' Modified packages/bar/bar_android/pubspec.yaml', - ' Modified packages/foo/pubspec.yaml', - ])); - expect( - output, - isNot(contains( - ' Modified packages/bar/bar_platform_interface/pubspec.yaml'))); - - expect( - simplePackage.pubspecFile.readAsLinesSync(), - containsAllInOrder([ - '# FOR TESTING ONLY. DO NOT MERGE.', - 'dependency_overrides:', - ' bar:', - ' path: ../bar/bar', - ' bar_platform_interface:', - ' path: ../bar/bar_platform_interface', - ])); - expect( - pluginAppFacing.pubspecFile.readAsLinesSync(), - containsAllInOrder([ - 'dependency_overrides:', - ' bar_platform_interface:', - ' path: ../../bar/bar_platform_interface', - ])); - }); - - test('rewrites "dev_dependencies" references', () async { - createFakePackage('foo', packagesDir); - final RepositoryPackage builderPackage = - createFakePackage('foo_builder', packagesDir); - - addDevDependenciesSection(builderPackage, [ - 'foo', - ]); - - final List output = await runCapturingPrint( - runner, ['make-deps-path-based', '--target-dependencies=foo']); - - expect( - output, - containsAll([ - 'Rewriting references to: foo...', - ' Modified packages/foo_builder/pubspec.yaml', - ])); - - expect( - builderPackage.pubspecFile.readAsLinesSync(), - containsAllInOrder([ - '# FOR TESTING ONLY. DO NOT MERGE.', - 'dependency_overrides:', - ' foo:', - ' path: ../foo', - ])); - }); - - test( - 'alphabetizes overrides from different sectinos to avoid lint warnings in analysis', - () async { - createFakePackage('a', packagesDir); - createFakePackage('b', packagesDir); - createFakePackage('c', packagesDir); - final RepositoryPackage targetPackage = - createFakePackage('target', packagesDir); - - addDependencies(targetPackage, ['a', 'c']); - addDevDependenciesSection(targetPackage, ['b']); - - final List output = await runCapturingPrint(runner, - ['make-deps-path-based', '--target-dependencies=c,a,b']); - - expect( - output, - containsAllInOrder([ - 'Rewriting references to: c, a, b...', - ' Modified packages/target/pubspec.yaml', - ])); - - expect( - targetPackage.pubspecFile.readAsLinesSync(), - containsAllInOrder([ - '# FOR TESTING ONLY. DO NOT MERGE.', - 'dependency_overrides:', - ' a:', - ' path: ../a', - ' b:', - ' path: ../b', - ' c:', - ' path: ../c', - ])); - }); - - // This test case ensures that running CI using this command on an interim - // PR that itself used this command won't fail on the rewrite step. - test('running a second time no-ops without failing', () async { - final RepositoryPackage simplePackage = - createFakePackage('foo', packagesDir, isFlutter: true); - final Directory pluginGroup = packagesDir.childDirectory('bar'); - - createFakePackage('bar_platform_interface', pluginGroup, isFlutter: true); - final RepositoryPackage pluginImplementation = - createFakePlugin('bar_android', pluginGroup); - final RepositoryPackage pluginAppFacing = - createFakePlugin('bar', pluginGroup); - - addDependencies(simplePackage, [ - 'bar', - 'bar_android', - 'bar_platform_interface', - ]); - addDependencies(pluginAppFacing, [ - 'bar_platform_interface', - 'bar_android', - ]); - addDependencies(pluginImplementation, [ - 'bar_platform_interface', - ]); - - await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies=bar,bar_platform_interface' - ]); - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies=bar,bar_platform_interface' - ]); - - expect( - output, - containsAll([ - 'Rewriting references to: bar, bar_platform_interface...', - ' Skipped packages/bar/bar/pubspec.yaml - Already rewritten', - ' Skipped packages/bar/bar_android/pubspec.yaml - Already rewritten', - ' Skipped packages/foo/pubspec.yaml - Already rewritten', - ])); - }); - - group('target-dependencies-with-non-breaking-updates', () { - test('no-ops for no published changes', () async { - final RepositoryPackage package = createFakePackage('foo', packagesDir); - - final String changedFileOutput = [ - package.pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - // Simulate no change to the version in the interface's pubspec.yaml. - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: package.pubspecFile.readAsStringSync()), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains('No target dependencies'), - ]), - ); - }); - - test('no-ops for no deleted packages', () async { - final String changedFileOutput = [ - // A change for a file that's not on disk simulates a deletion. - packagesDir.childDirectory('foo').childFile('pubspec.yaml'), - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains('Skipping foo; deleted.'), - contains('No target dependencies'), - ]), - ); - }); - - test('includes bugfix version changes as targets', () async { - const String newVersion = '1.0.1'; - final RepositoryPackage package = - createFakePackage('foo', packagesDir, version: newVersion); - - final File pubspecFile = package.pubspecFile; - final String changedFileOutput = [ - pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - final String gitPubspecContents = - pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); - // Simulate no change to the version in the interface's pubspec.yaml. - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: gitPubspecContents), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains('Rewriting references to: foo...'), - ]), - ); - }); - - test('includes minor version changes to 1.0+ as targets', () async { - const String newVersion = '1.1.0'; - final RepositoryPackage package = - createFakePackage('foo', packagesDir, version: newVersion); - - final File pubspecFile = package.pubspecFile; - final String changedFileOutput = [ - pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - final String gitPubspecContents = - pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); - // Simulate no change to the version in the interface's pubspec.yaml. - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: gitPubspecContents), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains('Rewriting references to: foo...'), - ]), - ); - }); - - test('does not include major version changes as targets', () async { - const String newVersion = '2.0.0'; - final RepositoryPackage package = - createFakePackage('foo', packagesDir, version: newVersion); - - final File pubspecFile = package.pubspecFile; - final String changedFileOutput = [ - pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - final String gitPubspecContents = - pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); - // Simulate no change to the version in the interface's pubspec.yaml. - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: gitPubspecContents), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains('No target dependencies'), - ]), - ); - }); - - test('does not include minor version changes to 0.x as targets', () async { - const String newVersion = '0.8.0'; - final RepositoryPackage package = - createFakePackage('foo', packagesDir, version: newVersion); - - final File pubspecFile = package.pubspecFile; - final String changedFileOutput = [ - pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - final String gitPubspecContents = - pubspecFile.readAsStringSync().replaceAll(newVersion, '0.7.0'); - // Simulate no change to the version in the interface's pubspec.yaml. - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: gitPubspecContents), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains('No target dependencies'), - ]), - ); - }); - - test('skips anything outside of the packages directory', () async { - final Directory toolDir = packagesDir.parent.childDirectory('tool'); - const String newVersion = '1.1.0'; - final RepositoryPackage package = createFakePackage( - 'flutter_plugin_tools', toolDir, - version: newVersion); - - // Simulate a minor version change so it would be a target. - final File pubspecFile = package.pubspecFile; - final String changedFileOutput = [ - pubspecFile, - ].map((File file) => file.path).join('\n'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: changedFileOutput), - ]; - final String gitPubspecContents = - pubspecFile.readAsStringSync().replaceAll(newVersion, '1.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: gitPubspecContents), - ]; - - final List output = await runCapturingPrint(runner, [ - 'make-deps-path-based', - '--target-dependencies-with-non-breaking-updates' - ]); - - expect( - output, - containsAllInOrder([ - contains( - 'Skipping /tool/flutter_plugin_tools/pubspec.yaml; not in packages directory.'), - contains('No target dependencies'), - ]), - ); - }); - }); -} diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart deleted file mode 100644 index f6333ebd367d..000000000000 --- a/script/tool/test/mocks.dart +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:file/file.dart'; -import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; - -class MockPlatform extends Mock implements Platform { - MockPlatform({ - this.isLinux = false, - this.isMacOS = false, - this.isWindows = false, - }); - - @override - bool isLinux; - - @override - bool isMacOS; - - @override - bool isWindows; - - @override - Uri get script => isWindows - ? Uri.file(r'C:\foo\bar', windows: true) - : Uri.file('/foo/bar', windows: false); - - @override - Map environment = {}; -} - -class MockProcess extends Mock implements io.Process { - /// Creates a mock process with the given results. - /// - /// The default encodings match the ProcessRunner defaults; mocks for - /// processes run with a different encoding will need to be created with - /// the matching encoding. - MockProcess({ - int exitCode = 0, - String? stdout, - String? stderr, - Encoding stdoutEncoding = io.systemEncoding, - Encoding stderrEncoding = io.systemEncoding, - }) : _exitCode = exitCode { - if (stdout != null) { - _stdoutController.add(stdoutEncoding.encoder.convert(stdout)); - } - if (stderr != null) { - _stderrController.add(stderrEncoding.encoder.convert(stderr)); - } - _stdoutController.close(); - _stderrController.close(); - } - - final int _exitCode; - final StreamController> _stdoutController = - StreamController>(); - final StreamController> _stderrController = - StreamController>(); - final MockIOSink stdinMock = MockIOSink(); - - @override - int get pid => 99; - - @override - Future get exitCode async => _exitCode; - - @override - Stream> get stdout => _stdoutController.stream; - - @override - Stream> get stderr => _stderrController.stream; - - @override - IOSink get stdin => stdinMock; -} - -class MockIOSink extends Mock implements IOSink { - List lines = []; - - @override - void writeln([Object? obj = '']) => lines.add(obj.toString()); -} diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart deleted file mode 100644 index f24d014bbfea..000000000000 --- a/script/tool/test/native_test_command_test.dart +++ /dev/null @@ -1,1796 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/cmake.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/file_utils.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/native_test_command.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -const String _androidIntegrationTestFilter = - '-Pandroid.testInstrumentationRunnerArguments.' - 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; - -final Map _kDeviceListMap = { - 'runtimes': >[ - { - 'bundlePath': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', - 'buildversion': '17L255', - 'runtimeRoot': - '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', - 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', - 'version': '13.4', - 'isAvailable': true, - 'name': 'iOS 13.4' - }, - ], - 'devices': { - 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ - { - 'dataPath': - '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', - 'logPath': - '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', - 'isAvailable': true, - 'deviceTypeIdentifier': - 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', - 'state': 'Shutdown', - 'name': 'iPhone 8 Plus' - } - ] - } -}; - -const String _fakeCmakeCommand = 'path/to/cmake'; - -void _createFakeCMakeCache(RepositoryPackage plugin, Platform platform) { - final CMakeProject project = CMakeProject(getExampleDir(plugin), - platform: platform, buildMode: 'Release'); - final File cache = project.buildDirectory.childFile('CMakeCache.txt'); - cache.createSync(recursive: true); - cache.writeAsStringSync('CMAKE_COMMAND:INTERNAL=$_fakeCmakeCommand'); -} - -// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of -// doing all the process mocking and validation. -void main() { - const String kDestination = '--ios-destination'; - - group('test native_test_command on Posix', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - // iOS and macOS tests expect macOS, Linux tests expect Linux; nothing - // needs to distinguish between Linux and macOS, so set both to true to - // allow them to share a setup group. - mockPlatform = MockPlatform(isMacOS: true, isLinux: true); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final NativeTestCommand command = NativeTestCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform); - - runner = CommandRunner( - 'native_test_command', 'Test for native_test_command'); - runner.addCommand(command); - }); - - // Returns a MockProcess to provide for "xcrun xcodebuild -list" for a - // project that contains [targets]. - MockProcess getMockXcodebuildListProcess(List targets) { - final Map projects = { - 'project': { - 'targets': targets, - } - }; - return MockProcess(stdout: jsonEncode(projects)); - } - - // Returns the ProcessCall to expect for checking the targets present in - // the [package]'s [platform]/Runner.xcodeproj. - ProcessCall getTargetCheckCall(Directory package, String platform) { - return ProcessCall( - 'xcrun', - [ - 'xcodebuild', - '-list', - '-json', - '-project', - package - .childDirectory(platform) - .childDirectory('Runner.xcodeproj') - .path, - ], - null); - } - - // Returns the ProcessCall to expect for running the tests in the - // workspace [platform]/Runner.xcworkspace, with the given extra flags. - ProcessCall getRunTestCall( - Directory package, - String platform, { - String? destination, - List extraFlags = const [], - }) { - return ProcessCall( - 'xcrun', - [ - 'xcodebuild', - 'test', - '-workspace', - '$platform/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - if (destination != null) ...['-destination', destination], - ...extraFlags, - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - package.path); - } - - // Returns the ProcessCall to expect for build the Linux unit tests for the - // given plugin. - ProcessCall getLinuxBuildCall(RepositoryPackage plugin) { - return ProcessCall( - 'cmake', - [ - '--build', - getExampleDir(plugin) - .childDirectory('build') - .childDirectory('linux') - .childDirectory('x64') - .childDirectory('release') - .path, - '--target', - 'unit_tests' - ], - null); - } - - test('fails if no platforms are provided', () async { - Error? commandError; - final List output = await runCapturingPrint( - runner, ['native-test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('At least one platform flag must be provided.'), - ]), - ); - }); - - test('fails if all test types are disabled', () async { - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - '--no-unit', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('At least one test type must be enabled.'), - ]), - ); - }); - - test('reports skips with no tests', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess(['RunnerTests', 'RunnerUITests']), - // Exit code 66 from testing indicates no tests. - MockProcess(exitCode: 66), - ]; - final List output = await runCapturingPrint( - runner, ['native-test', '--macos', '--no-unit']); - - expect( - output, - containsAllInOrder([ - contains('No tests found.'), - contains('Skipped 1 package(s)'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - getRunTestCall(pluginExampleDirectory, 'macos', - extraFlags: ['-only-testing:RunnerUITests']), - ])); - }); - - group('iOS', () { - test('skip if iOS is not supported', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final List output = await runCapturingPrint(runner, - ['native-test', '--ios', kDestination, 'foo_destination']); - expect( - output, - containsAllInOrder([ - contains('No implementation for iOS.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ])); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip if iOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.federated) - }); - - final List output = await runCapturingPrint(runner, - ['native-test', '--ios', kDestination, 'foo_destination']); - expect( - output, - containsAllInOrder([ - contains('No implementation for iOS.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ])); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('running with correct destination', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--ios', - kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Successfully ran iOS xctest for plugin/example') - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'ios'), - getRunTestCall(pluginExampleDirectory, 'ios', - destination: 'foo_destination'), - ])); - }); - - test('Not specifying --ios-destination assigns an available simulator', - () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(stdout: jsonEncode(_kDeviceListMap)), // simctl - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - await runCapturingPrint(runner, ['native-test', '--ios']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - const ProcessCall( - 'xcrun', - [ - 'simctl', - 'list', - 'devices', - 'runtimes', - 'available', - '--json', - ], - null), - getTargetCheckCall(pluginExampleDirectory, 'ios'), - getRunTestCall(pluginExampleDirectory, 'ios', - destination: 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A'), - ])); - }); - }); - - group('macOS', () { - test('skip if macOS is not supported', () async { - createFakePlugin('plugin', packagesDir); - - final List output = - await runCapturingPrint(runner, ['native-test', '--macos']); - - expect( - output, - containsAllInOrder([ - contains('No implementation for macOS.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ])); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip if macOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.federated), - }); - - final List output = - await runCapturingPrint(runner, ['native-test', '--macos']); - - expect( - output, - containsAllInOrder([ - contains('No implementation for macOS.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ])); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('runs for macOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - ]); - - expect( - output, - contains( - contains('Successfully ran macOS xctest for plugin/example'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - getRunTestCall(pluginExampleDirectory, 'macos'), - ])); - }); - }); - - group('Android', () { - test('runs Java unit tests in Android implementation folder', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'android/src/test/example_test.java', - ], - ); - - await runCapturingPrint(runner, ['native-test', '--android']); - - final Directory androidFolder = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], - androidFolder.path, - ), - ]), - ); - }); - - test('runs Java unit tests in example folder', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - ], - ); - - await runCapturingPrint(runner, ['native-test', '--android']); - - final Directory androidFolder = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], - androidFolder.path, - ), - ]), - ); - }); - - test('runs Java integration tests', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - - await runCapturingPrint( - runner, ['native-test', '--android', '--no-unit']); - - final Directory androidFolder = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const [ - 'app:connectedAndroidTest', - _androidIntegrationTestFilter, - ], - androidFolder.path, - ), - ]), - ); - }); - - test( - 'ignores Java integration test files associated with integration_test', - () async { - createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java', - 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java', - 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/MainActivityTest.java', - ], - ); - - await runCapturingPrint( - runner, ['native-test', '--android', '--no-unit']); - - // Nothing should run since those files are all - // integration_test-specific. - expect( - processRunner.recordedCalls, - orderedEquals([]), - ); - }); - - test('runs all tests when present', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'android/src/test/example_test.java', - 'example/android/gradlew', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - - await runCapturingPrint(runner, ['native-test', '--android']); - - final Directory androidFolder = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], - androidFolder.path, - ), - ProcessCall( - androidFolder.childFile('gradlew').path, - const [ - 'app:connectedAndroidTest', - _androidIntegrationTestFilter, - ], - androidFolder.path, - ), - ]), - ); - }); - - test('honors --no-unit', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'android/src/test/example_test.java', - 'example/android/gradlew', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - - await runCapturingPrint( - runner, ['native-test', '--android', '--no-unit']); - - final Directory androidFolder = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const [ - 'app:connectedAndroidTest', - _androidIntegrationTestFilter, - ], - androidFolder.path, - ), - ]), - ); - }); - - test('honors --no-integration', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'android/src/test/example_test.java', - 'example/android/gradlew', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - - await runCapturingPrint( - runner, ['native-test', '--android', '--no-integration']); - - final Directory androidFolder = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], - androidFolder.path, - ), - ]), - ); - }); - - test('fails when the app needs to be built', () async { - createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/app/src/test/example_test.java', - ], - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['native-test', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('ERROR: Run "flutter build apk" on plugin/example'), - contains('plugin:\n' - ' Examples must be built before testing.') - ]), - ); - }); - - test('logs missing test types', () async { - // No unit tests. - createFakePlugin( - 'plugin1', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - // No integration tests. - createFakePlugin( - 'plugin2', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'android/src/test/example_test.java', - 'example/android/gradlew', - ], - ); - - final List output = await runCapturingPrint( - runner, ['native-test', '--android'], - errorHandler: (Error e) { - // Having no unit tests is fatal, but that's not the point of this - // test so just ignore the failure. - }); - - expect( - output, - containsAllInOrder([ - contains('No Android unit tests found for plugin1/example'), - contains('Running integration tests...'), - contains( - 'No Android integration tests found for plugin2/example'), - contains('Running unit tests...'), - ])); - }); - - test('fails when a unit test fails', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - ], - ); - - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['native-test', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('plugin/example unit tests failed.'), - contains('The following packages had errors:'), - contains('plugin') - ]), - ); - }); - - test('fails when an integration test fails', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(), // unit passes - MockProcess(exitCode: 1), // integration fails - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['native-test', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('plugin/example integration tests failed.'), - contains('The following packages had errors:'), - contains('plugin') - ]), - ); - }); - - test('fails if there are no unit tests', () async { - createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/androidTest/IntegrationTest.java', - ], - ); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['native-test', '--android'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('No Android unit tests found for plugin/example'), - contains( - 'No unit tests ran. Plugins are required to have unit tests.'), - contains('The following packages had errors:'), - contains('plugin:\n' - ' No unit tests ran (use --exclude if this is intentional).') - ]), - ); - }); - - test('skips if Android is not supported', () async { - createFakePlugin( - 'plugin', - packagesDir, - ); - - final List output = await runCapturingPrint( - runner, ['native-test', '--android']); - - expect( - output, - containsAllInOrder([ - contains('No implementation for Android.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ]), - ); - }); - - test('skips when running no tests in integration-only mode', () async { - createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline) - }, - ); - - final List output = await runCapturingPrint( - runner, ['native-test', '--android', '--no-unit']); - - expect( - output, - containsAllInOrder([ - contains('No Android integration tests found for plugin/example'), - contains('SKIPPING: No tests found.'), - ]), - ); - }); - }); - - group('Linux', () { - test('builds and runs unit tests', () async { - const String testBinaryRelativePath = - 'build/linux/x64/release/bar/plugin_test'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/$testBinaryRelativePath' - ], platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - final File testBinary = childFileWithSubcomponents(plugin.directory, - ['example', ...testBinaryRelativePath.split('/')]); - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--linux', - '--no-integration', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running plugin_test...'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getLinuxBuildCall(plugin), - ProcessCall(testBinary.path, const [], null), - ])); - }); - - test('only runs release unit tests', () async { - const String debugTestBinaryRelativePath = - 'build/linux/x64/debug/bar/plugin_test'; - const String releaseTestBinaryRelativePath = - 'build/linux/x64/release/bar/plugin_test'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/$debugTestBinaryRelativePath', - 'example/$releaseTestBinaryRelativePath' - ], platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - final File releaseTestBinary = childFileWithSubcomponents( - plugin.directory, - ['example', ...releaseTestBinaryRelativePath.split('/')]); - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--linux', - '--no-integration', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running plugin_test...'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getLinuxBuildCall(plugin), - ProcessCall(releaseTestBinary.path, const [], null), - ])); - }); - - test('fails if CMake has not been configured', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--linux', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('plugin:\n' - ' Examples must be built before testing.') - ]), - ); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('fails if there are no unit tests', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--linux', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No test binaries found.'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getLinuxBuildCall(plugin), - ])); - }); - - test('fails if a unit test fails', () async { - const String testBinaryRelativePath = - 'build/linux/x64/release/bar/plugin_test'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/$testBinaryRelativePath' - ], platformSupport: { - platformLinux: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - final File testBinary = childFileWithSubcomponents(plugin.directory, - ['example', ...testBinaryRelativePath.split('/')]); - - processRunner.mockProcessesForExecutable[testBinary.path] = - [MockProcess(exitCode: 1)]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--linux', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running plugin_test...'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getLinuxBuildCall(plugin), - ProcessCall(testBinary.path, const [], null), - ])); - }); - }); - - // Tests behaviors of implementation that is shared between iOS and macOS. - group('iOS/macOS', () { - test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = - await runCapturingPrint(runner, ['native-test', '--macos'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin'), - ]), - ); - }); - - test('honors unit-only', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - '--no-integration', - ]); - - expect( - output, - contains( - contains('Successfully ran macOS xctest for plugin/example'))); - - // --no-integration should translate to '-only-testing:RunnerTests'. - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - getRunTestCall(pluginExampleDirectory, 'macos', - extraFlags: ['-only-testing:RunnerTests']), - ])); - }); - - test('honors integration-only', () async { - final RepositoryPackage plugin1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin1); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - '--no-unit', - ]); - - expect( - output, - contains( - contains('Successfully ran macOS xctest for plugin/example'))); - - // --no-unit should translate to '-only-testing:RunnerUITests'. - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - getRunTestCall(pluginExampleDirectory, 'macos', - extraFlags: ['-only-testing:RunnerUITests']), - ])); - }); - - test('skips when the requested target is not present', () async { - final RepositoryPackage plugin1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin1); - - // Simulate a project with unit tests but no integration tests... - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess(['RunnerTests']), - ]; - - // ... then try to run only integration tests. - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - '--no-unit', - ]); - - expect( - output, - containsAllInOrder([ - contains( - 'No "RunnerUITests" target in plugin/example; skipping.'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - ])); - }); - - test('fails if there are no unit tests', () async { - final RepositoryPackage plugin1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin1); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess(['RunnerUITests']), - ]; - - Error? commandError; - final List output = - await runCapturingPrint(runner, ['native-test', '--macos'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No "RunnerTests" target in plugin/example; skipping.'), - contains( - 'No unit tests ran. Plugins are required to have unit tests.'), - contains('The following packages had errors:'), - contains('plugin:\n' - ' No unit tests ran (use --exclude if this is intentional).'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - ])); - }); - - test('fails if unable to check for requested target', () async { - final RepositoryPackage plugin1 = createFakePlugin( - 'plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin1); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1), // xcodebuild -list - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to check targets for plugin/example.'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - ])); - }); - }); - - group('multiplatform', () { - test('runs all platfroms when supported', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: [ - 'example/android/gradlew', - 'android/src/test/example_test.java', - ], - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - final Directory androidFolder = - pluginExampleDirectory.childDirectory('android'); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), // iOS list - MockProcess(), // iOS run - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), // macOS list - MockProcess(), // macOS run - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--android', - '--ios', - '--macos', - kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAll([ - contains('Running Android tests for plugin/example'), - contains('Successfully ran iOS xctest for plugin/example'), - contains('Successfully ran macOS xctest for plugin/example'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(androidFolder.childFile('gradlew').path, - const ['testDebugUnitTest'], androidFolder.path), - getTargetCheckCall(pluginExampleDirectory, 'ios'), - getRunTestCall(pluginExampleDirectory, 'ios', - destination: 'foo_destination'), - getTargetCheckCall(pluginExampleDirectory, 'macos'), - getRunTestCall(pluginExampleDirectory, 'macos'), - ])); - }); - - test('runs only macOS for a macOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--ios', - '--macos', - kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('No implementation for iOS.'), - contains('Successfully ran macOS xctest for plugin/example'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'macos'), - getRunTestCall(pluginExampleDirectory, 'macos'), - ])); - }); - - test('runs only iOS for a iOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--ios', - '--macos', - kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('No implementation for macOS.'), - contains('Successfully ran iOS xctest for plugin/example') - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getTargetCheckCall(pluginExampleDirectory, 'ios'), - getRunTestCall(pluginExampleDirectory, 'ios', - destination: 'foo_destination'), - ])); - }); - - test('skips when nothing is supported', () async { - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--android', - '--ios', - '--macos', - '--windows', - kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('No implementation for Android.'), - contains('No implementation for iOS.'), - contains('No implementation for macOS.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ])); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skips Dart-only plugins', () async { - createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline, - hasDartCode: true, hasNativeCode: false), - platformWindows: const PlatformDetails(PlatformSupport.inline, - hasDartCode: true, hasNativeCode: false), - }, - ); - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--macos', - '--windows', - kDestination, - 'foo_destination', - ]); - - expect( - output, - containsAllInOrder([ - contains('No native code for macOS.'), - contains('No native code for Windows.'), - contains('SKIPPING: Nothing to test for target platform(s).'), - ])); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('failing one platform does not stop the tests', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - ], - ); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - getMockXcodebuildListProcess( - ['RunnerTests', 'RunnerUITests']), - ]; - - // Simulate failing Android, but not iOS. - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--android', - '--ios', - '--ios-destination', - 'foo_destination', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('Running tests for Android...'), - contains('plugin/example unit tests failed.'), - contains('Running tests for iOS...'), - contains('Successfully ran iOS xctest for plugin/example'), - contains('The following packages had errors:'), - contains('plugin:\n' - ' Android') - ]), - ); - }); - - test('failing multiple platforms reports multiple failures', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - extraFiles: [ - 'example/android/gradlew', - 'example/android/app/src/test/example_test.java', - ], - ); - - // Simulate failing Android. - final String gradlewPath = plugin - .getExamples() - .first - .platformDirectory(FlutterPlatform.android) - .childFile('gradlew') - .path; - processRunner.mockProcessesForExecutable[gradlewPath] = [ - MockProcess(exitCode: 1) - ]; - // Simulate failing Android. - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--android', - '--ios', - '--ios-destination', - 'foo_destination', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder([ - contains('Running tests for Android...'), - contains('Running tests for iOS...'), - contains('The following packages had errors:'), - contains('plugin:\n' - ' Android\n' - ' iOS') - ]), - ); - }); - }); - }); - - group('test native_test_command on Windows', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); - mockPlatform = MockPlatform(isWindows: true); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final NativeTestCommand command = NativeTestCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform); - - runner = CommandRunner( - 'native_test_command', 'Test for native_test_command'); - runner.addCommand(command); - }); - - // Returns the ProcessCall to expect for build the Windows unit tests for - // the given plugin. - ProcessCall getWindowsBuildCall(RepositoryPackage plugin) { - return ProcessCall( - _fakeCmakeCommand, - [ - '--build', - getExampleDir(plugin) - .childDirectory('build') - .childDirectory('windows') - .path, - '--target', - 'unit_tests', - '--config', - 'Debug' - ], - null); - } - - group('Windows', () { - test('runs unit tests', () async { - const String testBinaryRelativePath = - 'build/windows/Debug/bar/plugin_test.exe'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/$testBinaryRelativePath' - ], platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - final File testBinary = childFileWithSubcomponents(plugin.directory, - ['example', ...testBinaryRelativePath.split('/')]); - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--windows', - '--no-integration', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running plugin_test.exe...'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getWindowsBuildCall(plugin), - ProcessCall(testBinary.path, const [], null), - ])); - }); - - test('only runs debug unit tests', () async { - const String debugTestBinaryRelativePath = - 'build/windows/Debug/bar/plugin_test.exe'; - const String releaseTestBinaryRelativePath = - 'build/windows/Release/bar/plugin_test.exe'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/$debugTestBinaryRelativePath', - 'example/$releaseTestBinaryRelativePath' - ], platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - final File debugTestBinary = childFileWithSubcomponents( - plugin.directory, - ['example', ...debugTestBinaryRelativePath.split('/')]); - - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--windows', - '--no-integration', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running plugin_test.exe...'), - contains('No issues found!'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getWindowsBuildCall(plugin), - ProcessCall(debugTestBinary.path, const [], null), - ])); - }); - - test('fails if CMake has not been configured', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--windows', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('plugin:\n' - ' Examples must be built before testing.') - ]), - ); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('fails if there are no unit tests', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--windows', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No test binaries found.'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getWindowsBuildCall(plugin), - ])); - }); - - test('fails if a unit test fails', () async { - const String testBinaryRelativePath = - 'build/windows/Debug/bar/plugin_test.exe'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, extraFiles: [ - 'example/$testBinaryRelativePath' - ], platformSupport: { - platformWindows: const PlatformDetails(PlatformSupport.inline), - }); - _createFakeCMakeCache(plugin, mockPlatform); - - final File testBinary = childFileWithSubcomponents(plugin.directory, - ['example', ...testBinaryRelativePath.split('/')]); - - processRunner.mockProcessesForExecutable[testBinary.path] = - [MockProcess(exitCode: 1)]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'native-test', - '--windows', - '--no-integration', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Running plugin_test.exe...'), - ]), - ); - - expect( - processRunner.recordedCalls, - orderedEquals([ - getWindowsBuildCall(plugin), - ProcessCall(testBinary.path, const [], null), - ])); - }); - }); - }); -} diff --git a/script/tool/test/podspec_check_command_test.dart b/script/tool/test/podspec_check_command_test.dart deleted file mode 100644 index c31ffd46a4b7..000000000000 --- a/script/tool/test/podspec_check_command_test.dart +++ /dev/null @@ -1,428 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/podspec_check_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -/// Adds a fake podspec to [plugin]'s [platform] directory. -/// -/// If [includeSwiftWorkaround] is set, the xcconfig additions to make Swift -/// libraries work in apps that have no Swift will be included. If -/// [scopeSwiftWorkaround] is set, it will be specific to the iOS configuration. -void _writeFakePodspec(RepositoryPackage plugin, String platform, - {bool includeSwiftWorkaround = false, bool scopeSwiftWorkaround = false}) { - final String pluginName = plugin.directory.basename; - final File file = plugin.directory - .childDirectory(platform) - .childFile('$pluginName.podspec'); - final String swiftWorkaround = includeSwiftWorkaround - ? ''' - s.${scopeSwiftWorkaround ? 'ios.' : ''}xcconfig = { - 'LIBRARY_SEARCH_PATHS' => '\$(TOOLCHAIN_DIR)/usr/lib/swift/\$(PLATFORM_NAME)/ \$(SDKROOT)/usr/lib/swift', - 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', - } -''' - : ''; - file.createSync(recursive: true); - file.writeAsStringSync(''' -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'shared_preferences_foundation' - s.version = '0.0.1' - s.summary = 'iOS and macOS implementation of the shared_preferences plugin.' - s.description = <<-DESC -Wraps NSUserDefaults, providing a persistent store for simple key-value pairs. - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_foundation' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/main/packages/shared_preferences/shared_preferences_foundation' } - s.source_files = 'Classes/**/*' - s.ios.dependency 'Flutter' - s.osx.dependency 'FlutterMacOS' - s.ios.deployment_target = '9.0' - s.osx.deployment_target = '10.11' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - $swiftWorkaround - s.swift_version = '5.0' - -end -'''); -} - -void main() { - group('PodspecCheckCommand', () { - FileSystem fileSystem; - late Directory packagesDir; - late CommandRunner runner; - late MockPlatform mockPlatform; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - - mockPlatform = MockPlatform(isMacOS: true); - processRunner = RecordingProcessRunner(); - final PodspecCheckCommand command = PodspecCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = - CommandRunner('podspec_test', 'Test for $PodspecCheckCommand'); - runner.addCommand(command); - }); - - test('only runs on macOS', () async { - createFakePlugin('plugin1', packagesDir, - extraFiles: ['plugin1.podspec']); - mockPlatform.isMacOS = false; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - processRunner.recordedCalls, - equals([]), - ); - - expect( - output, - containsAllInOrder( - [contains('only supported on macOS')], - )); - }); - - test('runs pod lib lint on a podspec', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin1', - packagesDir, - extraFiles: [ - 'bogus.dart', // Ignore non-podspecs. - ], - ); - _writeFakePodspec(plugin, 'ios'); - - processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess(stdout: 'Foo', stderr: 'Bar'), - MockProcess(), - ]; - - final List output = - await runCapturingPrint(runner, ['podspec-check']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('which', const ['pod'], packagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - plugin - .platformDirectory(FlutterPlatform.ios) - .childFile('plugin1.podspec') - .path, - '--configuration=Debug', - '--skip-tests', - '--use-modular-headers', - '--use-libraries' - ], - packagesDir.path), - ProcessCall( - 'pod', - [ - 'lib', - 'lint', - plugin - .platformDirectory(FlutterPlatform.ios) - .childFile('plugin1.podspec') - .path, - '--configuration=Debug', - '--skip-tests', - '--use-modular-headers', - ], - packagesDir.path), - ]), - ); - - expect(output, contains('Linting plugin1.podspec')); - expect(output, contains('Foo')); - expect(output, contains('Bar')); - }); - - test('fails if pod is missing', () async { - final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir); - _writeFakePodspec(plugin, 'ios'); - - // Simulate failure from `which pod`. - processRunner.mockProcessesForExecutable['which'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder( - [ - contains('Unable to find "pod". Make sure it is in your path.'), - ], - )); - }); - - test('fails if linting as a framework fails', () async { - final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir); - _writeFakePodspec(plugin, 'ios'); - - // Simulate failure from `pod`. - processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder( - [ - contains('The following packages had errors:'), - contains('plugin1:\n' - ' plugin1.podspec') - ], - )); - }); - - test('fails if linting as a static library fails', () async { - final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir); - _writeFakePodspec(plugin, 'ios'); - - // Simulate failure from the second call to `pod`. - processRunner.mockProcessesForExecutable['pod'] = [ - MockProcess(), - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder( - [ - contains('The following packages had errors:'), - contains('plugin1:\n' - ' plugin1.podspec') - ], - )); - }); - - test('fails if an iOS Swift plugin is missing the search paths workaround', - () async { - final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, - extraFiles: ['ios/Classes/SomeSwift.swift']); - _writeFakePodspec(plugin, 'ios'); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder( - [ - contains(r''' - s.xcconfig = { - 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', - 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', - }'''), - contains('The following packages had errors:'), - contains('plugin1:\n' - ' plugin1.podspec') - ], - )); - }); - - test( - 'fails if a shared-source Swift plugin is missing the search paths workaround', - () async { - final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, - extraFiles: ['darwin/Classes/SomeSwift.swift']); - _writeFakePodspec(plugin, 'darwin'); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['podspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - - expect( - output, - containsAllInOrder( - [ - contains(r''' - s.xcconfig = { - 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', - 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', - }'''), - contains('The following packages had errors:'), - contains('plugin1:\n' - ' plugin1.podspec') - ], - )); - }); - - test('does not require the search paths workaround for macOS plugins', - () async { - final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, - extraFiles: ['macos/Classes/SomeSwift.swift']); - _writeFakePodspec(plugin, 'macos'); - - final List output = - await runCapturingPrint(runner, ['podspec-check']); - - expect( - output, - containsAllInOrder( - [ - contains('Ran for 1 package(s)'), - ], - )); - }); - - test('does not require the search paths workaround for ObjC iOS plugins', - () async { - final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, - extraFiles: [ - 'ios/Classes/SomeObjC.h', - 'ios/Classes/SomeObjC.m' - ]); - _writeFakePodspec(plugin, 'ios'); - - final List output = - await runCapturingPrint(runner, ['podspec-check']); - - expect( - output, - containsAllInOrder( - [ - contains('Ran for 1 package(s)'), - ], - )); - }); - - test('passes if the search paths workaround is present', () async { - final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, - extraFiles: ['ios/Classes/SomeSwift.swift']); - _writeFakePodspec(plugin, 'ios', includeSwiftWorkaround: true); - - final List output = - await runCapturingPrint(runner, ['podspec-check']); - - expect( - output, - containsAllInOrder( - [ - contains('Ran for 1 package(s)'), - ], - )); - }); - - test('passes if the search paths workaround is present for iOS only', - () async { - final RepositoryPackage plugin = createFakePlugin('plugin1', packagesDir, - extraFiles: ['ios/Classes/SomeSwift.swift']); - _writeFakePodspec(plugin, 'ios', - includeSwiftWorkaround: true, scopeSwiftWorkaround: true); - - final List output = - await runCapturingPrint(runner, ['podspec-check']); - - expect( - output, - containsAllInOrder( - [ - contains('Ran for 1 package(s)'), - ], - )); - }); - - test('does not require the search paths workaround for Swift example code', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin1', packagesDir, extraFiles: [ - 'ios/Classes/SomeObjC.h', - 'ios/Classes/SomeObjC.m', - 'example/ios/Runner/AppDelegate.swift', - ]); - _writeFakePodspec(plugin, 'ios'); - - final List output = - await runCapturingPrint(runner, ['podspec-check']); - - expect( - output, - containsAllInOrder( - [ - contains('Ran for 1 package(s)'), - ], - )); - }); - - test('skips when there are no podspecs', () async { - createFakePlugin('plugin1', packagesDir); - - final List output = - await runCapturingPrint(runner, ['podspec-check']); - - expect( - output, - containsAllInOrder( - [contains('SKIPPING: No podspecs.')], - )); - }); - }); -} diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart deleted file mode 100644 index 575f8509fd25..000000000000 --- a/script/tool/test/publish_check_command_test.dart +++ /dev/null @@ -1,464 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/publish_check_command.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$PublishCheckCommand tests', () { - FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final PublishCheckCommand publishCheckCommand = PublishCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - runner.addCommand(publishCheckCommand); - }); - - test('publish check all packages', () async { - final RepositoryPackage plugin1 = createFakePlugin( - 'plugin_tools_test_package_a', - packagesDir, - examples: [], - ); - final RepositoryPackage plugin2 = createFakePlugin( - 'plugin_tools_test_package_b', - packagesDir, - examples: [], - ); - - await runCapturingPrint(runner, ['publish-check']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'flutter', - const ['pub', 'publish', '--', '--dry-run'], - plugin1.path), - ProcessCall( - 'flutter', - const ['pub', 'publish', '--', '--dry-run'], - plugin2.path), - ])); - }); - - test('publish prepares dependencies of examples (when present)', () async { - final RepositoryPackage plugin1 = createFakePlugin( - 'plugin_tools_test_package_a', - packagesDir, - examples: ['example1', 'example2'], - ); - final RepositoryPackage plugin2 = createFakePlugin( - 'plugin_tools_test_package_b', - packagesDir, - examples: [], - ); - - await runCapturingPrint(runner, ['publish-check']); - - // For plugin1, these are the expected pub get calls that will happen - final Iterable pubGetCalls = - plugin1.getExamples().map((RepositoryPackage example) { - return ProcessCall( - 'dart', - const ['pub', 'get'], - example.path, - ); - }); - - expect(pubGetCalls, hasLength(2)); - expect( - processRunner.recordedCalls, - orderedEquals([ - // plugin1 has 2 examples, so there's some 'dart pub get' calls. - ...pubGetCalls, - ProcessCall( - 'flutter', - const ['pub', 'publish', '--', '--dry-run'], - plugin1.path), - // plugin2 has no examples, so there's no extra 'dart pub get' calls. - ProcessCall( - 'flutter', - const ['pub', 'publish', '--', '--dry-run'], - plugin2.path), - ]), - ); - }); - - test('fail on negative test', () async { - createFakePlugin('plugin_tools_test_package_a', packagesDir); - - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(exitCode: 1, stdout: 'Some error from pub') - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['publish-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Some error from pub'), - contains('Unable to publish plugin_tools_test_package_a'), - ]), - ); - }); - - test('fail on bad pubspec', () async { - final RepositoryPackage package = createFakePlugin('c', packagesDir); - await package.pubspecFile.writeAsString('bad-yaml'); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['publish-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No valid pubspec found.'), - ]), - ); - }); - - test('fails if AUTHORS is missing', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - package.authorsFile.delete(); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['publish-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'No AUTHORS file found. Packages must include an AUTHORS file.'), - ]), - ); - }); - - test('does not require AUTHORS for third-party', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', - packagesDir.parent - .childDirectory('third_party') - .childDirectory('packages')); - package.authorsFile.delete(); - - final List output = - await runCapturingPrint(runner, ['publish-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for a_package'), - ]), - ); - }); - - test('pass on prerelease if --allow-pre-release flag is on', () async { - createFakePlugin('d', packagesDir); - - final MockProcess process = MockProcess( - exitCode: 1, - stdout: 'Package has 1 warning.\n' - 'Packages with an SDK constraint on a pre-release of the Dart ' - 'SDK should themselves be published as a pre-release version.'); - processRunner.mockProcessesForExecutable['flutter'] = [ - process, - ]; - - expect( - runCapturingPrint( - runner, ['publish-check', '--allow-pre-release']), - completes); - }); - - test('fail on prerelease if --allow-pre-release flag is off', () async { - createFakePlugin('d', packagesDir); - - final MockProcess process = MockProcess( - exitCode: 1, - stdout: 'Package has 1 warning.\n' - 'Packages with an SDK constraint on a pre-release of the Dart ' - 'SDK should themselves be published as a pre-release version.'); - processRunner.mockProcessesForExecutable['flutter'] = [ - process, - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['publish-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Packages with an SDK constraint on a pre-release of the Dart SDK'), - contains('Unable to publish d'), - ]), - ); - }); - - test('Success message on stderr is not printed as an error', () async { - createFakePlugin('d', packagesDir); - - processRunner.mockProcessesForExecutable['flutter'] = [ - MockProcess(stdout: 'Package has 0 warnings.'), - ]; - - final List output = - await runCapturingPrint(runner, ['publish-check']); - - expect(output, isNot(contains(contains('ERROR:')))); - }); - - test( - '--machine: Log JSON with status:no-publish and correct human message, if there are no packages need to be published. ', - () async { - const Map httpResponseA = { - 'name': 'a', - 'versions': [ - '0.0.1', - '0.1.0', - ], - }; - - const Map httpResponseB = { - 'name': 'b', - 'versions': [ - '0.0.1', - '0.1.0', - '0.2.0', - ], - }; - - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'no_publish_a.json') { - return http.Response(json.encode(httpResponseA), 200); - } else if (request.url.pathSegments.last == 'no_publish_b.json') { - return http.Response(json.encode(httpResponseB), 200); - } - return http.Response('', 500); - }); - final PublishCheckCommand command = PublishCheckCommand(packagesDir, - processRunner: processRunner, httpClient: mockClient); - - runner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - runner.addCommand(command); - - createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); - createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - - final List output = await runCapturingPrint( - runner, ['publish-check', '--machine']); - - expect(output.first, r''' -{ - "status": "no-publish", - "humanMessage": [ - "\n============================================================\n|| Running for no_publish_a\n============================================================\n", - "Package no_publish_a version: 0.1.0 has already be published on pub.", - "\n============================================================\n|| Running for no_publish_b\n============================================================\n", - "Package no_publish_b version: 0.2.0 has already be published on pub.", - "\n", - "------------------------------------------------------------", - "Run overview:", - " no_publish_a - ran", - " no_publish_b - ran", - "", - "Ran for 2 package(s)", - "\n", - "No issues found!" - ] -}'''); - }); - - test( - '--machine: Log JSON with status:needs-publish and correct human message, if there is at least 1 plugin needs to be published.', - () async { - const Map httpResponseA = { - 'name': 'a', - 'versions': [ - '0.0.1', - '0.1.0', - ], - }; - - const Map httpResponseB = { - 'name': 'b', - 'versions': [ - '0.0.1', - '0.1.0', - ], - }; - - final MockClient mockClient = MockClient((http.Request request) async { - if (request.url.pathSegments.last == 'no_publish_a.json') { - return http.Response(json.encode(httpResponseA), 200); - } else if (request.url.pathSegments.last == 'no_publish_b.json') { - return http.Response(json.encode(httpResponseB), 200); - } - return http.Response('', 500); - }); - final PublishCheckCommand command = PublishCheckCommand(packagesDir, - processRunner: processRunner, httpClient: mockClient); - - runner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - runner.addCommand(command); - - createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); - createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - - final List output = await runCapturingPrint( - runner, ['publish-check', '--machine']); - - expect(output.first, r''' -{ - "status": "needs-publish", - "humanMessage": [ - "\n============================================================\n|| Running for no_publish_a\n============================================================\n", - "Package no_publish_a version: 0.1.0 has already be published on pub.", - "\n============================================================\n|| Running for no_publish_b\n============================================================\n", - "Running pub publish --dry-run:", - "Package no_publish_b is able to be published.", - "\n", - "------------------------------------------------------------", - "Run overview:", - " no_publish_a - ran", - " no_publish_b - ran", - "", - "Ran for 2 package(s)", - "\n", - "No issues found!" - ] -}'''); - }); - - test( - '--machine: Log correct JSON, if there is at least 1 plugin contains error.', - () async { - const Map httpResponseA = { - 'name': 'a', - 'versions': [ - '0.0.1', - '0.1.0', - ], - }; - - const Map httpResponseB = { - 'name': 'b', - 'versions': [ - '0.0.1', - '0.1.0', - ], - }; - - final MockClient mockClient = MockClient((http.Request request) async { - print('url ${request.url}'); - print(request.url.pathSegments.last); - if (request.url.pathSegments.last == 'no_publish_a.json') { - return http.Response(json.encode(httpResponseA), 200); - } else if (request.url.pathSegments.last == 'no_publish_b.json') { - return http.Response(json.encode(httpResponseB), 200); - } - return http.Response('', 500); - }); - final PublishCheckCommand command = PublishCheckCommand(packagesDir, - processRunner: processRunner, httpClient: mockClient); - - runner = CommandRunner( - 'publish_check_command', - 'Test for publish-check command.', - ); - runner.addCommand(command); - - final RepositoryPackage plugin = - createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); - createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); - - await plugin.pubspecFile.writeAsString('bad-yaml'); - - bool hasError = false; - final List output = await runCapturingPrint( - runner, ['publish-check', '--machine'], - errorHandler: (Error error) { - expect(error, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect(output.first, contains(r''' -{ - "status": "error", - "humanMessage": [ - "\n============================================================\n|| Running for no_publish_a\n============================================================\n", - "Failed to parse `pubspec.yaml` at /packages/no_publish_a/pubspec.yaml: ParsedYamlException:''')); - // This is split into two checks since the details of the YamlException - // aren't controlled by this package, so asserting its exact format would - // make the test fragile to irrelevant changes in those details. - expect(output.first, contains(r''' - "No valid pubspec found.", - "\n============================================================\n|| Running for no_publish_b\n============================================================\n", - "url https://pub.dev/packages/no_publish_b.json", - "no_publish_b.json", - "Running pub publish --dry-run:", - "Package no_publish_b is able to be published.", - "\n", - "The following packages had errors:", - " no_publish_a", - "See above for full details." - ] -}''')); - }); - }); -} diff --git a/script/tool/test/publish_command_test.dart b/script/tool/test/publish_command_test.dart deleted file mode 100644 index da5f9c871f05..000000000000 --- a/script/tool/test/publish_command_test.dart +++ /dev/null @@ -1,922 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/publish_command.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; -import 'package:mockito/mockito.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void main() { - final String flutterCommand = getFlutterCommand(const LocalPlatform()); - - late Directory packagesDir; - late MockGitDir gitDir; - late TestProcessRunner processRunner; - late CommandRunner commandRunner; - late MockStdin mockStdin; - late FileSystem fileSystem; - // Map of package name to mock response. - late Map> mockHttpResponses; - - void createMockCredentialFile() { - final String credentialPath = PublishCommand.getCredentialPath(); - fileSystem.file(credentialPath) - ..createSync(recursive: true) - ..writeAsStringSync('some credential'); - } - - setUp(() async { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = TestProcessRunner(); - - mockHttpResponses = >{}; - final MockClient mockClient = MockClient((http.Request request) async { - final String packageName = - request.url.pathSegments.last.replaceAll('.json', ''); - final Map? response = mockHttpResponses[packageName]; - if (response != null) { - return http.Response(json.encode(response), 200); - } - // Default to simulating the plugin never having been published. - return http.Response('', 404); - }); - - gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - // Route git calls through the process runner, to make mock output - // consistent with outer processes. Attach the first argument to the - // command to make targeting the mock results easier. - final String gitCommand = arguments.removeAt(0); - return processRunner.run('git-$gitCommand', arguments); - }); - - mockStdin = MockStdin(); - commandRunner = CommandRunner('tester', '') - ..addCommand(PublishCommand( - packagesDir, - processRunner: processRunner, - stdinput: mockStdin, - gitDir: gitDir, - httpClient: mockClient, - )); - }); - - group('Initial validation', () { - test('refuses to proceed with dirty files', () async { - final RepositoryPackage plugin = - createFakePlugin('foo', packagesDir, examples: []); - - processRunner.mockProcessesForExecutable['git-status'] = [ - MockProcess(stdout: '?? ${plugin.directory.childFile('tmp').path}\n') - ]; - - Error? commandError; - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains("There are files in the package directory that haven't " - 'been saved in git. Refusing to publish these files:\n\n' - '?? /packages/foo/tmp\n\n' - 'If the directory should be clean, you can run `git clean -xdf && ' - 'git reset --hard HEAD` to wipe all local changes.'), - contains('foo:\n' - ' uncommitted changes'), - ])); - }); - - test("fails immediately if the remote doesn't exist", () async { - createFakePlugin('foo', packagesDir, examples: []); - - processRunner.mockProcessesForExecutable['git-remote'] = [ - MockProcess(exitCode: 1), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - commandRunner, ['publish', '--packages=foo'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Unable to find URL for remote upstream; cannot push tags'), - ])); - }); - }); - - group('Publishes package', () { - test('while showing all output from pub publish to the user', () async { - createFakePlugin('plugin1', packagesDir, examples: []); - createFakePlugin('plugin2', packagesDir, examples: []); - - processRunner.mockProcessesForExecutable[flutterCommand] = [ - MockProcess( - stdout: 'Foo', - stderr: 'Bar', - stdoutEncoding: utf8, - stderrEncoding: utf8), // pub publish for plugin1 - MockProcess( - stdout: 'Baz', - stdoutEncoding: utf8, - stderrEncoding: utf8), // pub publish for plugin1 - ]; - - final List output = await runCapturingPrint( - commandRunner, ['publish', '--packages=plugin1,plugin2']); - - expect( - output, - containsAllInOrder([ - contains('Running `pub publish ` in /packages/plugin1...'), - contains('Foo'), - contains('Bar'), - contains('Package published!'), - contains('Running `pub publish ` in /packages/plugin2...'), - contains('Baz'), - contains('Package published!'), - ])); - }); - - test('forwards input from the user to `pub publish`', () async { - createFakePlugin('foo', packagesDir, examples: []); - - mockStdin.mockUserInputs.add(utf8.encode('user input')); - - await runCapturingPrint( - commandRunner, ['publish', '--packages=foo']); - - expect(processRunner.mockPublishProcess.stdinMock.lines, - contains('user input')); - }); - - test('forwards --pub-publish-flags to pub publish', () async { - final RepositoryPackage plugin = - createFakePlugin('foo', packagesDir, examples: []); - - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - '--pub-publish-flags', - '--dry-run,--server=bar' - ]); - - expect( - processRunner.recordedCalls, - contains(ProcessCall( - flutterCommand, - const ['pub', 'publish', '--dry-run', '--server=bar'], - plugin.path))); - }); - - test( - '--skip-confirmation flag automatically adds --force to --pub-publish-flags', - () async { - createMockCredentialFile(); - final RepositoryPackage plugin = - createFakePlugin('foo', packagesDir, examples: []); - - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - '--skip-confirmation', - '--pub-publish-flags', - '--server=bar' - ]); - - expect( - processRunner.recordedCalls, - contains(ProcessCall( - flutterCommand, - const ['pub', 'publish', '--server=bar', '--force'], - plugin.path))); - }); - - test('--force is only added once, regardless of plugin count', () async { - createMockCredentialFile(); - final RepositoryPackage plugin1 = - createFakePlugin('plugin_a', packagesDir, examples: []); - final RepositoryPackage plugin2 = - createFakePlugin('plugin_b', packagesDir, examples: []); - - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=plugin_a,plugin_b', - '--skip-confirmation', - '--pub-publish-flags', - '--server=bar' - ]); - - expect( - processRunner.recordedCalls, - containsAllInOrder([ - ProcessCall( - flutterCommand, - const ['pub', 'publish', '--server=bar', '--force'], - plugin1.path), - ProcessCall( - flutterCommand, - const ['pub', 'publish', '--server=bar', '--force'], - plugin2.path), - ])); - }); - - test('throws if pub publish fails', () async { - createFakePlugin('foo', packagesDir, examples: []); - - processRunner.mockProcessesForExecutable[flutterCommand] = [ - MockProcess(exitCode: 128) // pub publish - ]; - - Error? commandError; - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Publishing foo failed.'), - ])); - }); - - test('publish, dry run', () async { - final RepositoryPackage plugin = - createFakePlugin('foo', packagesDir, examples: []); - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - '--dry-run', - ]); - - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - expect( - output, - containsAllInOrder([ - contains('=============== DRY RUN ==============='), - contains('Running for foo'), - contains('Running `pub publish ` in ${plugin.path}...'), - contains('Tagging release foo-v0.0.1...'), - contains('Pushing tag to upstream...'), - contains('Published foo successfully!'), - ])); - }); - - test('can publish non-flutter package', () async { - const String packageName = 'a_package'; - createFakePackage(packageName, packagesDir); - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=$packageName', - ]); - - expect( - output, - containsAllInOrder( - [ - contains('Running `pub publish ` in /packages/a_package...'), - contains('Package published!'), - ], - ), - ); - }); - }); - - group('Tags release', () { - test('with the version and name from the pubspec.yaml', () async { - createFakePlugin('foo', packagesDir, examples: []); - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - ]); - - expect(processRunner.recordedCalls, - contains(const ProcessCall('git-tag', ['foo-v0.0.1'], null))); - }); - - test('only if publishing succeeded', () async { - createFakePlugin('foo', packagesDir, examples: []); - - processRunner.mockProcessesForExecutable[flutterCommand] = [ - MockProcess(exitCode: 128) // pub publish - ]; - - Error? commandError; - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Publishing foo failed.'), - ])); - expect( - processRunner.recordedCalls, - isNot(contains( - const ProcessCall('git-tag', ['foo-v0.0.1'], null)))); - }); - }); - - group('Pushes tags', () { - test('to upstream by default', () async { - createFakePlugin('foo', packagesDir, examples: []); - - mockStdin.readLineOutput = 'y'; - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - ]); - - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'foo-v0.0.1'], null))); - expect( - output, - containsAllInOrder([ - contains('Pushing tag to upstream...'), - contains('Published foo successfully!'), - ])); - }); - - test('does not ask for user input if the --skip-confirmation flag is on', - () async { - createMockCredentialFile(); - createFakePlugin('foo', packagesDir, examples: []); - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--skip-confirmation', - '--packages=foo', - ]); - - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'foo-v0.0.1'], null))); - expect( - output, - containsAllInOrder([ - contains('Published foo successfully!'), - ])); - }); - - test('to upstream by default, dry run', () async { - final RepositoryPackage plugin = - createFakePlugin('foo', packagesDir, examples: []); - - mockStdin.readLineOutput = 'y'; - - final List output = await runCapturingPrint( - commandRunner, ['publish', '--packages=foo', '--dry-run']); - - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - expect( - output, - containsAllInOrder([ - contains('=============== DRY RUN ==============='), - contains('Running `pub publish ` in ${plugin.path}...'), - contains('Tagging release foo-v0.0.1...'), - contains('Pushing tag to upstream...'), - contains('Published foo successfully!'), - ])); - }); - - test('to different remotes based on a flag', () async { - createFakePlugin('foo', packagesDir, examples: []); - - mockStdin.readLineOutput = 'y'; - - final List output = - await runCapturingPrint(commandRunner, [ - 'publish', - '--packages=foo', - '--remote', - 'origin', - ]); - - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['origin', 'foo-v0.0.1'], null))); - expect( - output, - containsAllInOrder([ - contains('Published foo successfully!'), - ])); - }); - }); - - group('Auto release (all-changed flag)', () { - test('can release newly created plugins', () async { - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': [], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': [], - }; - - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - // federated - final RepositoryPackage plugin2 = createFakePlugin( - 'plugin2', - packagesDir.childDirectory('plugin2'), - ); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - mockStdin.readLineOutput = 'y'; - - final List output = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - - expect( - output, - containsAllInOrder([ - contains( - 'Publishing all packages that have changed relative to "HEAD~"'), - contains('Running `pub publish ` in ${plugin1.path}...'), - contains('Running `pub publish ` in ${plugin2.path}...'), - contains('plugin1 - \x1B[32mpublished\x1B[0m'), - contains('plugin2/plugin2 - \x1B[32mpublished\x1B[0m'), - ])); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); - }); - - test('can release newly created plugins, while there are existing plugins', - () async { - mockHttpResponses['plugin0'] = { - 'name': 'plugin0', - 'versions': ['0.0.1'], - }; - - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': [], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': [], - }; - - // The existing plugin. - createFakePlugin('plugin0', packagesDir); - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - // federated - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - - // Git results for plugin0 having been released already, and plugin1 and - // plugin2 being new. - processRunner.mockProcessesForExecutable['git-tag'] = [ - MockProcess(stdout: 'plugin0-v0.0.1\n') - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - - mockStdin.readLineOutput = 'y'; - - final List output = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - - expect( - output, - containsAllInOrder([ - 'Running `pub publish ` in ${plugin1.path}...\n', - 'Running `pub publish ` in ${plugin2.path}...\n', - ])); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); - }); - - test('can release newly created plugins, dry run', () async { - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': [], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': [], - }; - - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - // federated - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - mockStdin.readLineOutput = 'y'; - - final List output = await runCapturingPrint( - commandRunner, [ - 'publish', - '--all-changed', - '--base-sha=HEAD~', - '--dry-run' - ]); - - expect( - output, - containsAllInOrder([ - contains('=============== DRY RUN ==============='), - contains('Running `pub publish ` in ${plugin1.path}...'), - contains('Tagging release plugin1-v0.0.1...'), - contains('Pushing tag to upstream...'), - contains('Published plugin1 successfully!'), - contains('Running `pub publish ` in ${plugin2.path}...'), - contains('Tagging release plugin2-v0.0.1...'), - contains('Pushing tag to upstream...'), - contains('Published plugin2 successfully!'), - ])); - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - }); - - test('version change triggers releases.', () async { - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': ['0.0.1'], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': ['0.0.1'], - }; - - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); - // federated - final RepositoryPackage plugin2 = createFakePlugin( - 'plugin2', packagesDir.childDirectory('plugin2'), - version: '0.0.2'); - - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - - mockStdin.readLineOutput = 'y'; - - final List output2 = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - expect( - output2, - containsAllInOrder([ - contains('Running `pub publish ` in ${plugin1.path}...'), - contains('Published plugin1 successfully!'), - contains('Running `pub publish ` in ${plugin2.path}...'), - contains('Published plugin2 successfully!'), - ])); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin2-v0.0.2'], null))); - }); - - test( - 'delete package will not trigger publish but exit the command successfully!', - () async { - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': ['0.0.1'], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': ['0.0.1'], - }; - - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); - // federated - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - plugin2.directory.deleteSync(recursive: true); - - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - - mockStdin.readLineOutput = 'y'; - - final List output2 = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - expect( - output2, - containsAllInOrder([ - contains('Running `pub publish ` in ${plugin1.path}...'), - contains('Published plugin1 successfully!'), - contains( - 'The pubspec file for plugin2/plugin2 does not exist, so no publishing will happen.\nSafe to ignore if the package is deleted in this commit.\n'), - contains('SKIPPING: package deleted'), - contains('skipped (with warning)'), - ])); - expect( - processRunner.recordedCalls, - contains(const ProcessCall( - 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); - }); - - test('Existing versions do not trigger release, also prints out message.', - () async { - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': ['0.0.2'], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': ['0.0.2'], - }; - - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); - // federated - final RepositoryPackage plugin2 = createFakePlugin( - 'plugin2', packagesDir.childDirectory('plugin2'), - version: '0.0.2'); - - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - processRunner.mockProcessesForExecutable['git-tag'] = [ - MockProcess( - stdout: 'plugin1-v0.0.2\n' - 'plugin2-v0.0.2\n') - ]; - - final List output = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - - expect( - output, - containsAllInOrder([ - contains('plugin1 0.0.2 has already been published'), - contains('SKIPPING: already published'), - contains('plugin2 0.0.2 has already been published'), - contains('SKIPPING: already published'), - ])); - - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - }); - - test( - 'Existing versions do not trigger release, but fail if the tags do not exist.', - () async { - mockHttpResponses['plugin1'] = { - 'name': 'plugin1', - 'versions': ['0.0.2'], - }; - - mockHttpResponses['plugin2'] = { - 'name': 'plugin2', - 'versions': ['0.0.2'], - }; - - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir, version: '0.0.2'); - // federated - final RepositoryPackage plugin2 = createFakePlugin( - 'plugin2', packagesDir.childDirectory('plugin2'), - version: '0.0.2'); - - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.pubspecFile.path}\n' - '${plugin2.pubspecFile.path}\n') - ]; - - Error? commandError; - final List output = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('plugin1 0.0.2 has already been published, ' - 'however the git release tag (plugin1-v0.0.2) was not found.'), - contains('plugin2 0.0.2 has already been published, ' - 'however the git release tag (plugin2-v0.0.2) was not found.'), - ])); - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - }); - - test('No version change does not release any plugins', () async { - // Non-federated - final RepositoryPackage plugin1 = - createFakePlugin('plugin1', packagesDir); - // federated - final RepositoryPackage plugin2 = - createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); - - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess( - stdout: '${plugin1.libDirectory.childFile('plugin1.dart').path}\n' - '${plugin2.libDirectory.childFile('plugin2.dart').path}\n') - ]; - - final List output = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - - expect(output, containsAllInOrder(['Ran for 0 package(s)'])); - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - }); - - test('Do not release flutter_plugin_tools', () async { - mockHttpResponses['plugin1'] = { - 'name': 'flutter_plugin_tools', - 'versions': [], - }; - - final RepositoryPackage flutterPluginTools = - createFakePlugin('flutter_plugin_tools', packagesDir); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: flutterPluginTools.pubspecFile.path) - ]; - - final List output = await runCapturingPrint(commandRunner, - ['publish', '--all-changed', '--base-sha=HEAD~']); - - expect( - output, - containsAllInOrder([ - contains( - 'SKIPPING: publishing flutter_plugin_tools via the tool is not supported') - ])); - expect( - output.contains( - 'Running `pub publish ` in ${flutterPluginTools.path}...', - ), - isFalse); - expect( - processRunner.recordedCalls - .map((ProcessCall call) => call.executable), - isNot(contains('git-push'))); - }); - }); -} - -/// An extension of [RecordingProcessRunner] that stores 'flutter pub publish' -/// calls so that their input streams can be checked in tests. -class TestProcessRunner extends RecordingProcessRunner { - // Most recent returned publish process. - late MockProcess mockPublishProcess; - - @override - Future start(String executable, List args, - {Directory? workingDirectory}) async { - final io.Process process = - await super.start(executable, args, workingDirectory: workingDirectory); - if (executable == getFlutterCommand(const LocalPlatform()) && - args.isNotEmpty && - args[0] == 'pub' && - args[1] == 'publish') { - mockPublishProcess = process as MockProcess; - } - return process; - } -} - -class MockStdin extends Mock implements io.Stdin { - List> mockUserInputs = >[]; - final StreamController> _controller = StreamController>(); - String? readLineOutput; - - @override - Stream transform(StreamTransformer, S> streamTransformer) { - mockUserInputs.forEach(_addUserInputsToSteam); - return _controller.stream.transform(streamTransformer); - } - - @override - StreamSubscription> listen(void Function(List event)? onData, - {Function? onError, void Function()? onDone, bool? cancelOnError}) { - return _controller.stream.listen(onData, - onError: onError, onDone: onDone, cancelOnError: cancelOnError); - } - - @override - String? readLineSync( - {Encoding encoding = io.systemEncoding, - bool retainNewlines = false}) => - readLineOutput; - - void _addUserInputsToSteam(List input) => _controller.add(input); -} diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart deleted file mode 100644 index 2c254ca94984..000000000000 --- a/script/tool/test/pubspec_check_command_test.dart +++ /dev/null @@ -1,982 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/pubspec_check_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -/// Returns the top section of a pubspec.yaml for a package named [name], -/// for either a flutter/packages or flutter/plugins package depending on -/// the values of [isPlugin]. -/// -/// By default it will create a header that includes all of the expected -/// values, elements can be changed via arguments to create incorrect -/// entries. -/// -/// If [includeRepository] is true, by default the path in the link will -/// be "packages/[name]"; a different "packages"-relative path can be -/// provided with [repositoryPackagesDirRelativePath]. -String _headerSection( - String name, { - bool isPlugin = false, - bool includeRepository = true, - String repositoryBranch = 'main', - String? repositoryPackagesDirRelativePath, - bool includeHomepage = false, - bool includeIssueTracker = true, - bool publishable = true, - String? description, -}) { - final String repositoryPath = repositoryPackagesDirRelativePath ?? name; - final List repoLinkPathComponents = [ - 'flutter', - if (isPlugin) 'plugins' else 'packages', - 'tree', - repositoryBranch, - 'packages', - repositoryPath, - ]; - final String repoLink = - 'https://github.com/${repoLinkPathComponents.join('/')}'; - final String issueTrackerLink = 'https://github.com/flutter/flutter/issues?' - 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; - description ??= 'A test package for validating that the pubspec.yaml ' - 'follows repo best practices.'; - return ''' -name: $name -description: $description -${includeRepository ? 'repository: $repoLink' : ''} -${includeHomepage ? 'homepage: $repoLink' : ''} -${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} -version: 1.0.0 -${publishable ? '' : "publish_to: 'none'"} -'''; -} - -String _environmentSection() { - return ''' -environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.0.0" -'''; -} - -String _flutterSection({ - bool isPlugin = false, - String? implementedPackage, - Map> pluginPlatformDetails = - const >{}, -}) { - String pluginEntry = ''' - plugin: -${implementedPackage == null ? '' : ' implements: $implementedPackage'} - platforms: -'''; - - for (final MapEntry> platform - in pluginPlatformDetails.entries) { - pluginEntry += ''' - ${platform.key}: -'''; - for (final MapEntry detail in platform.value.entries) { - pluginEntry += ''' - ${detail.key}: ${detail.value} -'''; - } - } - - return ''' -flutter: -${isPlugin ? pluginEntry : ''} -'''; -} - -String _dependenciesSection() { - return ''' -dependencies: - flutter: - sdk: flutter -'''; -} - -String _devDependenciesSection() { - return ''' -dev_dependencies: - flutter_test: - sdk: flutter -'''; -} - -String _falseSecretsSection() { - return ''' -false_secrets: - - /lib/main.dart -'''; -} - -void main() { - group('test pubspec_check_command', () { - late CommandRunner runner; - late RecordingProcessRunner processRunner; - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = fileSystem.currentDirectory.childDirectory('packages'); - createPackagesDirectory(parentDir: packagesDir.parent); - processRunner = RecordingProcessRunner(); - final PubspecCheckCommand command = PubspecCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'pubspec_check_command', 'Test for pubspec_check_command'); - runner.addCommand(command); - }); - - test('passes for a plugin following conventions', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -${_falseSecretsSection()} -'''); - - plugin.getExamples().first.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin_example', - publishable: false, - includeRepository: false, - includeIssueTracker: false, - )} -${_environmentSection()} -${_dependenciesSection()} -${_flutterSection()} -'''); - - final List output = await runCapturingPrint(runner, [ - 'pubspec-check', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin...'), - contains('Running for plugin/example...'), - contains('No issues found!'), - ]), - ); - }); - - test('passes for a Flutter package following conventions', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - package.pubspecFile.writeAsStringSync(''' -${_headerSection('a_package')} -${_environmentSection()} -${_dependenciesSection()} -${_devDependenciesSection()} -${_flutterSection()} -${_falseSecretsSection()} -'''); - - package.getExamples().first.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'a_package', - publishable: false, - includeRepository: false, - includeIssueTracker: false, - )} -${_environmentSection()} -${_dependenciesSection()} -${_flutterSection()} -'''); - - final List output = await runCapturingPrint(runner, [ - 'pubspec-check', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for a_package...'), - contains('Running for a_package/example...'), - contains('No issues found!'), - ]), - ); - }); - - test('passes for a minimal package following conventions', () async { - final RepositoryPackage package = - createFakePackage('package', packagesDir, examples: []); - - package.pubspecFile.writeAsStringSync(''' -${_headerSection('package')} -${_environmentSection()} -${_dependenciesSection()} -'''); - - final List output = await runCapturingPrint(runner, [ - 'pubspec-check', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for package...'), - contains('No issues found!'), - ]), - ); - }); - - test('fails when homepage is included', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, includeHomepage: true)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Found a "homepage" entry; only "repository" should be used.'), - ]), - ); - }); - - test('fails when repository is missing', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, includeRepository: false)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Missing "repository"'), - ]), - ); - }); - - test('fails when homepage is given instead of repository', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Found a "homepage" entry; only "repository" should be used.'), - ]), - ); - }); - - test('fails when repository package name is incorrect', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The "repository" link should end with the package path.'), - ]), - ); - }); - - test('fails when repository uses master instead of main', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, repositoryBranch: 'master')} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The "repository" link should use "main", not "master".'), - ]), - ); - }); - - test('fails when issue tracker is missing', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, includeIssueTracker: false)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('A package should have an "issue_tracker" link'), - ]), - ); - }); - - test('fails when description is too short', () async { - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', packagesDir.childDirectory('a_plugin'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, description: 'Too short')} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('"description" is too short. pub.dev recommends package ' - 'descriptions of 60-180 characters.'), - ]), - ); - }); - - test( - 'allows short descriptions for non-app-facing parts of federated plugins', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, description: 'Too short')} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('"description" is too short. pub.dev recommends package ' - 'descriptions of 60-180 characters.'), - ]), - ); - }); - - test('fails when description is too long', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - const String description = 'This description is too long. It just goes ' - 'on and on and on and on and on. pub.dev will down-score it because ' - 'there is just too much here. Someone shoul really cut this down to just ' - 'the core description so that search results are more useful and the ' - 'package does not lose pub points.'; - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true, description: description)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('"description" is too long. pub.dev recommends package ' - 'descriptions of 60-180 characters.'), - ]), - ); - }); - - test('fails when environment section is out of order', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true)} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -${_environmentSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Major sections should follow standard repository ordering:'), - ]), - ); - }); - - test('fails when flutter section is out of order', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true)} -${_flutterSection(isPlugin: true)} -${_environmentSection()} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Major sections should follow standard repository ordering:'), - ]), - ); - }); - - test('fails when dependencies section is out of order', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_devDependenciesSection()} -${_dependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Major sections should follow standard repository ordering:'), - ]), - ); - }); - - test('fails when dev_dependencies section is out of order', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true)} -${_environmentSection()} -${_devDependenciesSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Major sections should follow standard repository ordering:'), - ]), - ); - }); - - test('fails when false_secrets section is out of order', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin', isPlugin: true)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_falseSecretsSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Major sections should follow standard repository ordering:'), - ]), - ); - }); - - test('fails when an implemenation package is missing "implements"', - () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin_a_foo', isPlugin: true)} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Missing "implements: plugin_a" in "plugin" section.'), - ]), - ); - }); - - test('fails when an implemenation package has the wrong "implements"', - () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin_a_foo', isPlugin: true)} -${_environmentSection()} -${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Expecetd "implements: plugin_a"; ' - 'found "implements: plugin_a_foo".'), - ]), - ); - }); - - test('passes for a correct implemenation package', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin_a_foo', - isPlugin: true, - repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_foo', - )} -${_environmentSection()} -${_flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - final List output = - await runCapturingPrint(runner, ['pubspec-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin_a_foo...'), - contains('No issues found!'), - ]), - ); - }); - - test('fails when a "default_package" looks incorrect', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin_a', - isPlugin: true, - repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', - )} -${_environmentSection()} -${_flutterSection( - isPlugin: true, - pluginPlatformDetails: >{ - 'android': {'default_package': 'plugin_b_android'} - }, - )} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - '"plugin_b_android" is not an expected implementation name for "plugin_a"'), - ]), - ); - }); - - test( - 'fails when a "default_package" does not have a corresponding dependency', - () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin_a', - isPlugin: true, - repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', - )} -${_environmentSection()} -${_flutterSection( - isPlugin: true, - pluginPlatformDetails: >{ - 'android': {'default_package': 'plugin_a_android'} - }, - )} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following default_packages are missing corresponding ' - 'dependencies:\n plugin_a_android'), - ]), - ); - }); - - test('passes for an app-facing package without "implements"', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin_a', - isPlugin: true, - repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', - )} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - final List output = - await runCapturingPrint(runner, ['pubspec-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin_a/plugin_a...'), - contains('No issues found!'), - ]), - ); - }); - - test('passes for a platform interface package without "implements"', - () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a_platform_interface', packagesDir.childDirectory('plugin_a'), - examples: []); - - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin_a_platform_interface', - isPlugin: true, - repositoryPackagesDirRelativePath: - 'plugin_a/plugin_a_platform_interface', - )} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - final List output = - await runCapturingPrint(runner, ['pubspec-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin_a_platform_interface...'), - contains('No issues found!'), - ]), - ); - }); - - test('validates some properties even for unpublished packages', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin_a_foo', packagesDir.childDirectory('plugin_a'), - examples: []); - - // Environment section is in the wrong location. - // Missing 'implements'. - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection('plugin_a_foo', isPlugin: true, publishable: false)} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -${_environmentSection()} -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['pubspec-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - 'Major sections should follow standard repository ordering:'), - contains('Missing "implements: plugin_a" in "plugin" section.'), - ]), - ); - }); - - test('ignores some checks for unpublished packages', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, examples: []); - - // Missing metadata that is only useful for published packages, such as - // repository and issue tracker. - plugin.pubspecFile.writeAsStringSync(''' -${_headerSection( - 'plugin', - isPlugin: true, - publishable: false, - includeRepository: false, - includeIssueTracker: false, - )} -${_environmentSection()} -${_flutterSection(isPlugin: true)} -${_dependenciesSection()} -${_devDependenciesSection()} -'''); - - final List output = - await runCapturingPrint(runner, ['pubspec-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin...'), - contains('No issues found!'), - ]), - ); - }); - }); - - group('test pubspec_check_command on Windows', () { - late CommandRunner runner; - late RecordingProcessRunner processRunner; - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); - mockPlatform = MockPlatform(isWindows: true); - packagesDir = fileSystem.currentDirectory.childDirectory('packages'); - createPackagesDirectory(parentDir: packagesDir.parent); - processRunner = RecordingProcessRunner(); - final PubspecCheckCommand command = PubspecCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'pubspec_check_command', 'Test for pubspec_check_command'); - runner.addCommand(command); - }); - - test('repository check works', () async { - final RepositoryPackage package = - createFakePackage('package', packagesDir, examples: []); - - package.pubspecFile.writeAsStringSync(''' -${_headerSection('package')} -${_environmentSection()} -${_dependenciesSection()} -'''); - - final List output = - await runCapturingPrint(runner, ['pubspec-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for package...'), - contains('No issues found!'), - ]), - ); - }); - }); -} diff --git a/script/tool/test/readme_check_command_test.dart b/script/tool/test/readme_check_command_test.dart deleted file mode 100644 index eb2b6c8e7512..000000000000 --- a/script/tool/test/readme_check_command_test.dart +++ /dev/null @@ -1,741 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/readme_check_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late CommandRunner runner; - late RecordingProcessRunner processRunner; - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = fileSystem.currentDirectory.childDirectory('packages'); - createPackagesDirectory(parentDir: packagesDir.parent); - processRunner = RecordingProcessRunner(); - final ReadmeCheckCommand command = ReadmeCheckCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner( - 'readme_check_command', 'Test for readme_check_command'); - runner.addCommand(command); - }); - - test('prints paths of checked READMEs', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - examples: ['example1', 'example2']); - for (final RepositoryPackage example in package.getExamples()) { - example.readmeFile.writeAsStringSync('A readme'); - } - getExampleDir(package).childFile('README.md').writeAsStringSync('A readme'); - - final List output = - await runCapturingPrint(runner, ['readme-check']); - - expect( - output, - containsAll([ - contains(' Checking README.md...'), - contains(' Checking example/README.md...'), - contains(' Checking example/example1/README.md...'), - contains(' Checking example/example2/README.md...'), - ]), - ); - }); - - test('fails when package README is missing', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - package.readmeFile.deleteSync(); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Missing README.md'), - ]), - ); - }); - - test('passes when example README is missing', () async { - createFakePackage('a_package', packagesDir); - - final List output = - await runCapturingPrint(runner, ['readme-check']); - - expect( - output, - containsAllInOrder([ - contains('No README for example'), - ]), - ); - }); - - test('does not inculde non-example subpackages', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - const String subpackageName = 'special_test'; - final RepositoryPackage miscSubpackage = - createFakePackage(subpackageName, package.directory); - miscSubpackage.readmeFile.delete(); - - final List output = - await runCapturingPrint(runner, ['readme-check']); - - expect(output, isNot(contains(subpackageName))); - }); - - test('fails when README still has plugin template boilerplate', () async { - final RepositoryPackage package = createFakePlugin('a_plugin', packagesDir); - package.readmeFile.writeAsStringSync(''' -## Getting Started - -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/developing-packages/), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. - -For help getting started with Flutter development, view the -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The boilerplate section about getting started with Flutter ' - 'should not be left in.'), - contains('Contains template boilerplate'), - ]), - ); - }); - - test('fails when example README still has application template boilerplate', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - package.getExamples().first.readmeFile.writeAsStringSync(''' -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The boilerplate section about getting started with Flutter ' - 'should not be left in.'), - contains('Contains template boilerplate'), - ]), - ); - }); - - test( - 'fails when a plugin implementation package example README has the ' - 'template boilerplate', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin_ios', packagesDir.childDirectory('a_plugin')); - package.getExamples().first.readmeFile.writeAsStringSync(''' -# a_plugin_ios_example - -Demonstrates how to use the a_plugin_ios plugin. -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The boilerplate should not be left in for a federated plugin ' - "implementation package's example."), - contains('Contains template boilerplate'), - ]), - ); - }); - - test( - 'allows the template boilerplate in the example README for packages ' - 'other than plugin implementation packages', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', - packagesDir.childDirectory('a_plugin'), - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - }, - ); - // Write a README with an OS support table so that the main README check - // passes. - package.readmeFile.writeAsStringSync(''' -# a_plugin - -| | Android | -|----------------|---------| -| **Support** | SDK 19+ | - -A great plugin. -'''); - package.getExamples().first.readmeFile.writeAsStringSync(''' -# a_plugin_example - -Demonstrates how to use the a_plugin plugin. -'''); - - final List output = - await runCapturingPrint(runner, ['readme-check']); - - expect( - output, - containsAll([ - contains(' Checking README.md...'), - contains(' Checking example/README.md...'), - ]), - ); - }); - - test( - 'fails when a plugin implementation package example README does not have ' - 'the repo-standard message', () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin_ios', packagesDir.childDirectory('a_plugin')); - package.getExamples().first.readmeFile.writeAsStringSync(''' -# a_plugin_ios_example - -Some random description. -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The example README for a platform implementation package ' - 'should warn readers about its intended use. Please copy the ' - 'example README from another implementation package in this ' - 'repository.'), - contains('Missing implementation package example warning'), - ]), - ); - }); - - test('passes for a plugin implementation package with the expected content', - () async { - final RepositoryPackage package = createFakePlugin( - 'a_plugin', - packagesDir.childDirectory('a_plugin'), - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - }, - ); - // Write a README with an OS support table so that the main README check - // passes. - package.readmeFile.writeAsStringSync(''' -# a_plugin - -| | Android | -|----------------|---------| -| **Support** | SDK 19+ | - -A great plugin. -'''); - package.getExamples().first.readmeFile.writeAsStringSync(''' -# Platform Implementation Test App - -This is a test app for manual testing and automated integration testing -of this platform implementation. It is not intended to demonstrate actual use of -this package, since the intent is that plugin clients use the app-facing -package. - -Unless you are making changes to this implementation package, this example is -very unlikely to be relevant. -'''); - - final List output = - await runCapturingPrint(runner, ['readme-check']); - - expect( - output, - containsAll([ - contains(' Checking README.md...'), - contains(' Checking example/README.md...'), - ]), - ); - }); - - test( - 'fails when multi-example top-level example directory README still has ' - 'application template boilerplate', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, - examples: ['example1', 'example2']); - package.directory - .childDirectory('example') - .childFile('README.md') - .writeAsStringSync(''' -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The boilerplate section about getting started with Flutter ' - 'should not be left in.'), - contains('Contains template boilerplate'), - ]), - ); - }); - - group('plugin OS support', () { - test( - 'does not check support table for anything other than app-facing plugin packages', - () async { - const String federatedPluginName = 'a_federated_plugin'; - final Directory federatedDir = - packagesDir.childDirectory(federatedPluginName); - // A non-plugin package. - createFakePackage('a_package', packagesDir); - // Non-app-facing parts of a federated plugin. - createFakePlugin( - '${federatedPluginName}_platform_interface', federatedDir); - createFakePlugin('${federatedPluginName}_android', federatedDir); - - final List output = await runCapturingPrint(runner, [ - 'readme-check', - ]); - - expect( - output, - containsAll([ - contains('Running for a_package...'), - contains('Running for a_federated_plugin_platform_interface...'), - contains('Running for a_federated_plugin_android...'), - contains('No issues found!'), - ]), - ); - }); - - test('fails when non-federated plugin is missing an OS support table', - () async { - createFakePlugin('a_plugin', packagesDir); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No OS support table found'), - ]), - ); - }); - - test( - 'fails when app-facing part of a federated plugin is missing an OS support table', - () async { - createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No OS support table found'), - ]), - ); - }); - - test('fails the OS support table is missing the header', () async { - final RepositoryPackage plugin = - createFakePlugin('a_plugin', packagesDir); - - plugin.readmeFile.writeAsStringSync(''' -A very useful plugin. - -| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('OS support table does not have the expected header format'), - ]), - ); - }); - - test('fails if the OS support table is missing a supported OS', () async { - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - plugin.readmeFile.writeAsStringSync(''' -A very useful plugin. - -| | Android | iOS | -|----------------|---------|----------| -| **Support** | SDK 21+ | iOS 10+* | -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains(' OS support table does not match supported platforms:\n' - ' Actual: android, ios, web\n' - ' Documented: android, ios'), - contains('Incorrect OS support table'), - ]), - ); - }); - - test('fails if the OS support table lists an extra OS', () async { - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - }, - ); - - plugin.readmeFile.writeAsStringSync(''' -A very useful plugin. - -| | Android | iOS | Web | -|----------------|---------|----------|------------------------| -| **Support** | SDK 21+ | iOS 10+* | [See `camera_web `][1] | -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains(' OS support table does not match supported platforms:\n' - ' Actual: android, ios\n' - ' Documented: android, ios, web'), - contains('Incorrect OS support table'), - ]), - ); - }); - - test('fails if the OS support table has unexpected OS formatting', - () async { - final RepositoryPackage plugin = createFakePlugin( - 'a_plugin', - packagesDir, - platformSupport: { - platformAndroid: const PlatformDetails(PlatformSupport.inline), - platformIOS: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - plugin.readmeFile.writeAsStringSync(''' -A very useful plugin. - -| | android | ios | MacOS | web | -|----------------|---------|----------|-------|------------------------| -| **Support** | SDK 21+ | iOS 10+* | 10.11 | [See `camera_web `][1] | -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains(' Incorrect OS capitalization: android, ios, MacOS, web\n' - ' Please use standard capitalizations: Android, iOS, macOS, Web\n'), - contains('Incorrect OS support formatting'), - ]), - ); - }); - }); - - group('code blocks', () { - test('fails on missing info string', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - package.readmeFile.writeAsStringSync(''' -Example: - -``` -void main() { - // ... -} -``` -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Code block at line 3 is missing a language identifier.'), - contains('Missing language identifier for code block'), - ]), - ); - }); - - test('allows unknown info strings', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - package.readmeFile.writeAsStringSync(''' -Example: - -```someunknowninfotag -A B C -``` -'''); - - final List output = await runCapturingPrint(runner, [ - 'readme-check', - ]); - - expect( - output, - containsAll([ - contains('Running for a_package...'), - contains('No issues found!'), - ]), - ); - }); - - test('allows space around info strings', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - package.readmeFile.writeAsStringSync(''' -Example: - -``` dart -A B C -``` -'''); - - final List output = await runCapturingPrint(runner, [ - 'readme-check', - ]); - - expect( - output, - containsAll([ - contains('Running for a_package...'), - contains('No issues found!'), - ]), - ); - }); - - test('passes when excerpt requirement is met', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', - packagesDir, - extraFiles: [kReadmeExcerptConfigPath], - ); - - package.readmeFile.writeAsStringSync(''' -Example: - - -```dart -A B C -``` -'''); - - final List output = await runCapturingPrint( - runner, ['readme-check', '--require-excerpts']); - - expect( - output, - containsAll([ - contains('Running for a_package...'), - contains('No issues found!'), - ]), - ); - }); - - test('fails when excerpts are used but the package is not configured', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - package.readmeFile.writeAsStringSync(''' -Example: - - -```dart -A B C -``` -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check', '--require-excerpts'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('code-excerpt tag found, but the package is not configured ' - 'for excerpting. Follow the instructions at\n' - 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages\n' - 'for setting up a build.excerpt.yaml file.'), - contains('Missing code-excerpt configuration'), - ]), - ); - }); - - test('fails on missing excerpt tag when requested', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir); - - package.readmeFile.writeAsStringSync(''' -Example: - -```dart -A B C -``` -'''); - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['readme-check', '--require-excerpts'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Dart code block at line 3 is not managed by code-excerpt.'), - // Ensure that the failure message links to instructions. - contains( - 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages'), - contains('Missing code-excerpt management for code block'), - ]), - ); - }); - }); -} diff --git a/script/tool/test/remove_dev_dependencies_test.dart b/script/tool/test/remove_dev_dependencies_test.dart deleted file mode 100644 index 776cbf197838..000000000000 --- a/script/tool/test/remove_dev_dependencies_test.dart +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/remove_dev_dependencies.dart'; -import 'package:test/test.dart'; - -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late Directory packagesDir; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - - final RemoveDevDependenciesCommand command = RemoveDevDependenciesCommand( - packagesDir, - ); - runner = CommandRunner('trim_dev_dependencies_command', - 'Test for trim_dev_dependencies_command'); - runner.addCommand(command); - }); - - void addToPubspec(RepositoryPackage package, String addition) { - final String originalContent = package.pubspecFile.readAsStringSync(); - package.pubspecFile.writeAsStringSync(''' -$originalContent -$addition -'''); - } - - test('skips if nothing is removed', () async { - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - final List output = - await runCapturingPrint(runner, ['remove-dev-dependencies']); - - expect( - output, - containsAllInOrder([ - contains('SKIPPING: Nothing to remove.'), - ]), - ); - }); - - test('removes dev_dependencies', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - addToPubspec(package, ''' -dev_dependencies: - some_dependency: ^2.1.8 - another_dependency: ^1.0.0 -'''); - - final List output = - await runCapturingPrint(runner, ['remove-dev-dependencies']); - - expect( - output, - containsAllInOrder([ - contains('Removed dev_dependencies'), - ]), - ); - expect(package.pubspecFile.readAsStringSync(), - isNot(contains('some_dependency:'))); - expect(package.pubspecFile.readAsStringSync(), - isNot(contains('another_dependency:'))); - }); - - test('removes from examples', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - final RepositoryPackage example = package.getExamples().first; - addToPubspec(example, ''' -dev_dependencies: - some_dependency: ^2.1.8 - another_dependency: ^1.0.0 -'''); - - final List output = - await runCapturingPrint(runner, ['remove-dev-dependencies']); - - expect( - output, - containsAllInOrder([ - contains('Removed dev_dependencies'), - ]), - ); - expect(package.pubspecFile.readAsStringSync(), - isNot(contains('some_dependency:'))); - expect(package.pubspecFile.readAsStringSync(), - isNot(contains('another_dependency:'))); - }); -} diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart deleted file mode 100644 index 14a1e4a67c1f..000000000000 --- a/script/tool/test/test_command_test.dart +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/test_command.dart'; -import 'package:platform/platform.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -void main() { - group('$TestCommand', () { - late FileSystem fileSystem; - late Platform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final TestCommand command = TestCommand( - packagesDir, - processRunner: processRunner, - platform: mockPlatform, - ); - - runner = CommandRunner('test_test', 'Test for $TestCommand'); - runner.addCommand(command); - }); - - test('runs flutter test on each plugin', () async { - final RepositoryPackage plugin1 = createFakePlugin('plugin1', packagesDir, - extraFiles: ['test/empty_test.dart']); - final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, - extraFiles: ['test/empty_test.dart']); - - await runCapturingPrint(runner, ['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin1.path), - ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin2.path), - ]), - ); - }); - - test('runs flutter test on Flutter package example tests', () async { - final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir, - extraFiles: [ - 'test/empty_test.dart', - 'example/test/an_example_test.dart' - ]); - - await runCapturingPrint(runner, ['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin.path), - ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], getExampleDir(plugin).path), - ]), - ); - }); - - test('fails when Flutter tests fail', () async { - createFakePlugin('plugin1', packagesDir, - extraFiles: ['test/empty_test.dart']); - createFakePlugin('plugin2', packagesDir, - extraFiles: ['test/empty_test.dart']); - - processRunner - .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = - [ - MockProcess(exitCode: 1), // plugin 1 test - MockProcess(), // plugin 2 test - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin1'), - ])); - }); - - test('skips testing plugins without test directory', () async { - createFakePlugin('plugin1', packagesDir); - final RepositoryPackage plugin2 = createFakePlugin('plugin2', packagesDir, - extraFiles: ['test/empty_test.dart']); - - await runCapturingPrint(runner, ['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall(getFlutterCommand(mockPlatform), - const ['test', '--color'], plugin2.path), - ]), - ); - }); - - test('runs dart run test on non-Flutter packages', () async { - final RepositoryPackage plugin = createFakePlugin('a', packagesDir, - extraFiles: ['test/empty_test.dart']); - final RepositoryPackage package = createFakePackage('b', packagesDir, - extraFiles: ['test/empty_test.dart']); - - await runCapturingPrint( - runner, ['test', '--enable-experiment=exp1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['test', '--color', '--enable-experiment=exp1'], - plugin.path), - ProcessCall('dart', const ['pub', 'get'], package.path), - ProcessCall( - 'dart', - const ['run', '--enable-experiment=exp1', 'test'], - package.path), - ]), - ); - }); - - test('runs dart run test on non-Flutter package examples', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', packagesDir, extraFiles: [ - 'test/empty_test.dart', - 'example/test/an_example_test.dart' - ]); - - await runCapturingPrint(runner, ['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('dart', const ['pub', 'get'], package.path), - ProcessCall('dart', const ['run', 'test'], package.path), - ProcessCall('dart', const ['pub', 'get'], - getExampleDir(package).path), - ProcessCall('dart', const ['run', 'test'], - getExampleDir(package).path), - ]), - ); - }); - - test('fails when getting non-Flutter package dependencies fails', () async { - createFakePackage('a_package', packagesDir, - extraFiles: ['test/empty_test.dart']); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1), // dart pub get - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to fetch dependencies'), - contains('The following packages had errors:'), - contains(' a_package'), - ])); - }); - - test('fails when non-Flutter tests fail', () async { - createFakePackage('a_package', packagesDir, - extraFiles: ['test/empty_test.dart']); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(), // dart pub get - MockProcess(exitCode: 1), // dart pub run test - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['test'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' a_package'), - ])); - }); - - test('runs on Chrome for web plugins', () async { - final RepositoryPackage plugin = createFakePlugin( - 'plugin', - packagesDir, - extraFiles: ['test/empty_test.dart'], - platformSupport: { - platformWeb: const PlatformDetails(PlatformSupport.inline), - }, - ); - - await runCapturingPrint(runner, ['test']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['test', '--color', '--platform=chrome'], - plugin.path), - ]), - ); - }); - - test('enable-experiment flag', () async { - final RepositoryPackage plugin = createFakePlugin('a', packagesDir, - extraFiles: ['test/empty_test.dart']); - final RepositoryPackage package = createFakePackage('b', packagesDir, - extraFiles: ['test/empty_test.dart']); - - await runCapturingPrint( - runner, ['test', '--enable-experiment=exp1']); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - getFlutterCommand(mockPlatform), - const ['test', '--color', '--enable-experiment=exp1'], - plugin.path), - ProcessCall('dart', const ['pub', 'get'], package.path), - ProcessCall( - 'dart', - const ['run', '--enable-experiment=exp1', 'test'], - package.path), - ]), - ); - }); - }); -} diff --git a/script/tool/test/update_excerpts_command_test.dart b/script/tool/test/update_excerpts_command_test.dart deleted file mode 100644 index 5a2f0f340414..000000000000 --- a/script/tool/test/update_excerpts_command_test.dart +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/update_excerpts_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late Directory packagesDir; - late RecordingProcessRunner processRunner; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - final MockGitDir gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - processRunner = RecordingProcessRunner(); - final UpdateExcerptsCommand command = UpdateExcerptsCommand( - packagesDir, - processRunner: processRunner, - platform: MockPlatform(), - gitDir: gitDir, - ); - - runner = CommandRunner( - 'update_excerpts_command', 'Test for update_excerpts_command'); - runner.addCommand(command); - }); - - test('runs pub get before running scripts', () async { - final RepositoryPackage package = createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - final Directory example = getExampleDir(package); - - await runCapturingPrint(runner, ['update-excerpts']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall('dart', const ['pub', 'get'], example.path), - ProcessCall( - 'dart', - const [ - 'run', - 'build_runner', - 'build', - '--config', - 'excerpt', - '--output', - 'excerpts', - '--delete-conflicting-outputs', - ], - example.path), - ])); - }); - - test('runs when config is present', () async { - final RepositoryPackage package = createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - final Directory example = getExampleDir(package); - - final List output = - await runCapturingPrint(runner, ['update-excerpts']); - - expect( - processRunner.recordedCalls, - containsAll([ - ProcessCall( - 'dart', - const [ - 'run', - 'build_runner', - 'build', - '--config', - 'excerpt', - '--output', - 'excerpts', - '--delete-conflicting-outputs', - ], - example.path), - ProcessCall( - 'dart', - const [ - 'run', - 'code_excerpt_updater', - '--write-in-place', - '--yaml', - '--no-escape-ng-interpolation', - '../README.md', - ], - example.path), - ])); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('skips when no config is present', () async { - createFakePlugin('a_package', packagesDir); - - final List output = - await runCapturingPrint(runner, ['update-excerpts']); - - expect(processRunner.recordedCalls, isEmpty); - - expect( - output, - containsAllInOrder([ - contains('Skipped 1 package(s)'), - ])); - }); - - test('restores pubspec even if running the script fails', () async { - final RepositoryPackage package = createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1), // dart pub get - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['update-excerpts'], errorHandler: (Error e) { - commandError = e; - }); - - // Check that it's definitely a failure in a step between making the changes - // and restoring the original. - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package:\n' - ' Unable to get script dependencies') - ])); - - final String examplePubspecContent = - package.getExamples().first.pubspecFile.readAsStringSync(); - expect(examplePubspecContent, isNot(contains('code_excerpter'))); - expect(examplePubspecContent, isNot(contains('code_excerpt_updater'))); - }); - - test('fails if pub get fails', () async { - createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(exitCode: 1), // dart pub get - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['update-excerpts'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package:\n' - ' Unable to get script dependencies') - ])); - }); - - test('fails if extraction fails', () async { - createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(), // dart pub get - MockProcess(exitCode: 1), // dart run build_runner ... - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['update-excerpts'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package:\n' - ' Unable to extract excerpts') - ])); - }); - - test('fails if injection fails', () async { - createFakePlugin('a_package', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - processRunner.mockProcessesForExecutable['dart'] = [ - MockProcess(), // dart pub get - MockProcess(), // dart run build_runner ... - MockProcess(exitCode: 1), // dart run code_excerpt_updater ... - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['update-excerpts'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains('a_package:\n' - ' Unable to inject excerpts') - ])); - }); - - test('fails if READMEs are changed with --fail-on-change', () async { - createFakePlugin('a_plugin', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - const String changedFilePath = 'packages/a_plugin/README.md'; - processRunner.mockProcessesForExecutable['git'] = [ - MockProcess(stdout: changedFilePath), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['update-excerpts', '--fail-on-change'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('README.md is out of sync with its source excerpts'), - contains('Snippets are out of sync in the following files: ' - 'packages/a_plugin/README.md'), - ])); - }); - - test('passes if unrelated files are changed with --fail-on-change', () async { - createFakePlugin('a_plugin', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - const String changedFilePath = 'packages/a_plugin/linux/CMakeLists.txt'; - processRunner.mockProcessesForExecutable['git'] = [ - MockProcess(stdout: changedFilePath), - ]; - - final List output = await runCapturingPrint( - runner, ['update-excerpts', '--fail-on-change']); - - expect( - output, - containsAllInOrder([ - contains('Ran for 1 package(s)'), - ])); - }); - - test('fails if git ls-files fails', () async { - createFakePlugin('a_plugin', packagesDir, - extraFiles: [kReadmeExcerptConfigPath]); - - processRunner.mockProcessesForExecutable['git'] = [ - MockProcess(exitCode: 1) - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['update-excerpts', '--fail-on-change'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to determine local file state'), - ])); - }); -} diff --git a/script/tool/test/update_release_info_command_test.dart b/script/tool/test/update_release_info_command_test.dart deleted file mode 100644 index cfec93823ff0..000000000000 --- a/script/tool/test/update_release_info_command_test.dart +++ /dev/null @@ -1,674 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/update_release_info_command.dart'; -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void main() { - late FileSystem fileSystem; - late Directory packagesDir; - late MockGitDir gitDir; - late RecordingProcessRunner processRunner; - late CommandRunner runner; - - setUp(() { - fileSystem = MemoryFileSystem(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - - gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - // Route git calls through a process runner, to make mock output - // consistent with other processes. Attach the first argument to the - // command to make targeting the mock results easier. - final String gitCommand = arguments.removeAt(0); - return processRunner.run('git-$gitCommand', arguments); - }); - - final UpdateReleaseInfoCommand command = UpdateReleaseInfoCommand( - packagesDir, - gitDir: gitDir, - ); - runner = CommandRunner( - 'update_release_info_command', 'Test for update_release_info_command'); - runner.addCommand(command); - }); - - group('flags', () { - test('fails if --changelog is missing', () async { - Exception? commandError; - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - ], exceptionHandler: (Exception e) { - commandError = e; - }); - - expect(commandError, isA()); - }); - - test('fails if --changelog is blank', () async { - Exception? commandError; - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - '--changelog', - '', - ], exceptionHandler: (Exception e) { - commandError = e; - }); - - expect(commandError, isA()); - }); - - test('fails if --version is missing', () async { - Exception? commandError; - await runCapturingPrint( - runner, ['update-release-info', '--changelog', ''], - exceptionHandler: (Exception e) { - commandError = e; - }); - - expect(commandError, isA()); - }); - - test('fails if --version is an unknown value', () async { - Exception? commandError; - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=foo', - '--changelog', - '', - ], exceptionHandler: (Exception e) { - commandError = e; - }); - - expect(commandError, isA()); - }); - }); - - group('changelog', () { - test('adds new NEXT section', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## 1.0.0 - -* Previous changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - '--changelog', - 'A change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## NEXT - -* A change. - -$originalChangelog'''; - - expect( - output, - containsAllInOrder([ - contains(' Added a NEXT section.'), - ]), - ); - expect(newChangelog, expectedChangeLog); - }); - - test('adds to existing NEXT section', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## NEXT - -* Already-pending changes. - -## 1.0.0 - -* Old changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - '--changelog', - 'A change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## NEXT - -* A change. -* Already-pending changes. - -## 1.0.0 - -* Old changes. -'''; - - expect(output, - containsAllInOrder([contains(' Updated NEXT section.')])); - expect(newChangelog, expectedChangeLog); - }); - - test('adds new version section', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## 1.0.0 - -* Previous changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'A change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## 1.0.1 - -* A change. - -$originalChangelog'''; - - expect( - output, - containsAllInOrder([ - contains(' Added a 1.0.1 section.'), - ]), - ); - expect(newChangelog, expectedChangeLog); - }); - - test('converts existing NEXT section to version section', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## NEXT - -* Already-pending changes. - -## 1.0.0 - -* Old changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'A change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## 1.0.1 - -* A change. -* Already-pending changes. - -## 1.0.0 - -* Old changes. -'''; - - expect(output, - containsAllInOrder([contains(' Updated NEXT section.')])); - expect(newChangelog, expectedChangeLog); - }); - - test('treats multiple lines as multiple list items', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## 1.0.0 - -* Previous changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'First change.\nSecond change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## 1.0.1 - -* First change. -* Second change. - -$originalChangelog'''; - - expect(newChangelog, expectedChangeLog); - }); - - test('adds a period to any lines missing it, and removes whitespace', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## 1.0.0 - -* Previous changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'First change \nSecond change' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## 1.0.1 - -* First change. -* Second change. - -$originalChangelog'''; - - expect(newChangelog, expectedChangeLog); - }); - - test('handles non-standard changelog format', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -# 1.0.0 - -* A version with the wrong heading format. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - '--changelog', - 'A change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## NEXT - -* A change. - -$originalChangelog'''; - - expect(output, - containsAllInOrder([contains(' Added a NEXT section.')])); - expect(newChangelog, expectedChangeLog); - }); - - test('adds to existing NEXT section using - list style', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## NEXT - - - Already-pending changes. - -## 1.0.0 - - - Previous changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - '--changelog', - 'A change.' - ]); - - final String newChangelog = package.changelogFile.readAsStringSync(); - const String expectedChangeLog = ''' -## NEXT - - - A change. - - Already-pending changes. - -## 1.0.0 - - - Previous changes. -'''; - - expect(output, - containsAllInOrder([contains(' Updated NEXT section.')])); - expect(newChangelog, expectedChangeLog); - }); - - test('skips for "minimal" when there are no changes at all', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.1'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/different_package/lib/foo.dart -'''), - ]; - final String originalChangelog = package.changelogFile.readAsStringSync(); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minimal', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.0.1'); - expect(package.changelogFile.readAsStringSync(), originalChangelog); - expect( - output, - containsAllInOrder([ - contains('No changes to package'), - contains('Skipped 1 package') - ])); - }); - - test('skips for "minimal" when there are only test changes', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.1'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/a_package/test/a_test.dart -packages/a_package/example/integration_test/another_test.dart -'''), - ]; - final String originalChangelog = package.changelogFile.readAsStringSync(); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minimal', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.0.1'); - expect(package.changelogFile.readAsStringSync(), originalChangelog); - expect( - output, - containsAllInOrder([ - contains('No non-exempt changes to package'), - contains('Skipped 1 package') - ])); - }); - - test('fails if CHANGELOG.md is missing', () async { - createFakePackage('a_package', packagesDir, includeCommonFiles: false); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minor', - '--changelog', - 'A change.', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect(output, - containsAllInOrder([contains(' Missing CHANGELOG.md.')])); - }); - - test('fails if CHANGELOG.md has unexpected NEXT block format', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - const String originalChangelog = ''' -## NEXT - -Some free-form text that isn't a list. - -## 1.0.0 - -- Previous changes. -'''; - package.changelogFile.writeAsStringSync(originalChangelog); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minor', - '--changelog', - 'A change.', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains(' Existing NEXT section has unrecognized format.') - ])); - }); - }); - - group('pubspec', () { - test('does not change for --next', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.0'); - - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=next', - '--changelog', - 'A change.' - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.0.0'); - }); - - test('updates bugfix version for pre-1.0 without existing build number', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '0.1.0'); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '0.1.0+1'); - expect( - output, - containsAllInOrder( - [contains(' Incremented version to 0.1.0+1')])); - }); - - test('updates bugfix version for pre-1.0 with existing build number', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '0.1.0+2'); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '0.1.0+3'); - expect( - output, - containsAllInOrder( - [contains(' Incremented version to 0.1.0+3')])); - }); - - test('updates bugfix version for post-1.0', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.1'); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=bugfix', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.0.2'); - expect( - output, - containsAllInOrder( - [contains(' Incremented version to 1.0.2')])); - }); - - test('updates minor version for pre-1.0', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '0.1.0+2'); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minor', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '0.1.1'); - expect( - output, - containsAllInOrder( - [contains(' Incremented version to 0.1.1')])); - }); - - test('updates minor version for post-1.0', () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.1'); - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minor', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.1.0'); - expect( - output, - containsAllInOrder( - [contains(' Incremented version to 1.1.0')])); - }); - - test('updates bugfix version for "minimal" with publish-worthy changes', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.1'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/a_package/lib/plugin.dart -'''), - ]; - - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minimal', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.0.2'); - expect( - output, - containsAllInOrder( - [contains(' Incremented version to 1.0.2')])); - }); - - test('no version change for "minimal" with non-publish-worthy changes', - () async { - final RepositoryPackage package = - createFakePackage('a_package', packagesDir, version: '1.0.1'); - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/a_package/test/plugin_test.dart -'''), - ]; - - await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minimal', - '--changelog', - 'A change.', - ]); - - final String version = package.parsePubspec().version?.toString() ?? ''; - expect(version, '1.0.1'); - }); - - test('fails if there is no version in pubspec', () async { - createFakePackage('a_package', packagesDir, version: null); - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'update-release-info', - '--version=minor', - '--changelog', - 'A change.', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder( - [contains('Could not determine current version.')])); - }); - }); -} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart deleted file mode 100644 index 913242b6ea69..000000000000 --- a/script/tool/test/util.dart +++ /dev/null @@ -1,471 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/file_utils.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/common/process_runner.dart'; -import 'package:flutter_plugin_tools/src/common/repository_package.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as p; -import 'package:platform/platform.dart'; -import 'package:quiver/collection.dart'; - -import 'mocks.dart'; - -export 'package:flutter_plugin_tools/src/common/repository_package.dart'; - -/// The relative path from a package to the file that is used to enable -/// README excerpting for a package. -// This is a shared constant to ensure that both readme-check and -// update-excerpt are looking for the same file, so that readme-check can't -// get out of sync with what actually drives excerpting. -const String kReadmeExcerptConfigPath = 'example/build.excerpt.yaml'; - -const String _defaultDartConstraint = '>=2.14.0 <3.0.0'; -const String _defaultFlutterConstraint = '>=2.5.0'; - -/// Returns the exe name that command will use when running Flutter on -/// [platform]. -String getFlutterCommand(Platform platform) => - platform.isWindows ? 'flutter.bat' : 'flutter'; - -/// Creates a packages directory in the given location. -/// -/// If [parentDir] is set the packages directory will be created there, -/// otherwise [fileSystem] must be provided and it will be created an arbitrary -/// location in that filesystem. -Directory createPackagesDirectory( - {Directory? parentDir, FileSystem? fileSystem}) { - assert(parentDir != null || fileSystem != null, - 'One of parentDir or fileSystem must be provided'); - assert(fileSystem == null || fileSystem is MemoryFileSystem, - 'If using a real filesystem, parentDir must be provided'); - final Directory packagesDir = - (parentDir ?? fileSystem!.currentDirectory).childDirectory('packages'); - packagesDir.createSync(); - return packagesDir; -} - -/// Details for platform support in a plugin. -@immutable -class PlatformDetails { - const PlatformDetails( - this.type, { - this.hasNativeCode = true, - this.hasDartCode = false, - }); - - /// The type of support for the platform. - final PlatformSupport type; - - /// Whether or not the plugin includes native code. - /// - /// Ignored for web, which does not have native code. - final bool hasNativeCode; - - /// Whether or not the plugin includes Dart code. - /// - /// Ignored for web, which always has native code. - final bool hasDartCode; -} - -/// Returns the 'example' directory for [package]. -/// -/// This is deliberately not a method on [RepositoryPackage] since actual tool -/// code should essentially never need this, and instead be using -/// [RepositoryPackage.getExamples] to avoid assuming there's a single example -/// directory. However, needing to construct paths with the example directory -/// is very common in test code. -/// -/// This returns a Directory rather than a RepositoryPackage because there is no -/// guarantee that the returned directory is a package. -Directory getExampleDir(RepositoryPackage package) { - return package.directory.childDirectory('example'); -} - -/// Creates a plugin package with the given [name] in [packagesDirectory]. -/// -/// [platformSupport] is a map of platform string to the support details for -/// that platform. -/// -/// [extraFiles] is an optional list of plugin-relative paths, using Posix -/// separators, of extra files to create in the plugin. -RepositoryPackage createFakePlugin( - String name, - Directory parentDirectory, { - List examples = const ['example'], - List extraFiles = const [], - Map platformSupport = - const {}, - String? version = '0.0.1', - String flutterConstraint = _defaultFlutterConstraint, - String dartConstraint = _defaultDartConstraint, -}) { - final RepositoryPackage package = createFakePackage( - name, - parentDirectory, - isFlutter: true, - examples: examples, - extraFiles: extraFiles, - version: version, - flutterConstraint: flutterConstraint, - dartConstraint: dartConstraint, - ); - - createFakePubspec( - package, - name: name, - isPlugin: true, - platformSupport: platformSupport, - version: version, - flutterConstraint: flutterConstraint, - dartConstraint: dartConstraint, - ); - - return package; -} - -/// Creates a plugin package with the given [name] in [packagesDirectory]. -/// -/// [extraFiles] is an optional list of package-relative paths, using unix-style -/// separators, of extra files to create in the package. -/// -/// If [includeCommonFiles] is true, common but non-critical files like -/// CHANGELOG.md, README.md, and AUTHORS will be included. -/// -/// If non-null, [directoryName] will be used for the directory instead of -/// [name]. -RepositoryPackage createFakePackage( - String name, - Directory parentDirectory, { - List examples = const ['example'], - List extraFiles = const [], - bool isFlutter = false, - String? version = '0.0.1', - String flutterConstraint = _defaultFlutterConstraint, - String dartConstraint = _defaultDartConstraint, - bool includeCommonFiles = true, - String? directoryName, - String? publishTo, -}) { - final RepositoryPackage package = - RepositoryPackage(parentDirectory.childDirectory(directoryName ?? name)); - package.directory.createSync(recursive: true); - - package.libDirectory.createSync(); - createFakePubspec(package, - name: name, - isFlutter: isFlutter, - version: version, - flutterConstraint: flutterConstraint, - dartConstraint: dartConstraint); - if (includeCommonFiles) { - package.changelogFile.writeAsStringSync(''' -## $version - * Some changes. - '''); - package.readmeFile.writeAsStringSync('A very useful package'); - package.authorsFile.writeAsStringSync('Google Inc.'); - } - - if (examples.length == 1) { - createFakePackage('${name}_example', package.directory, - directoryName: examples.first, - examples: [], - includeCommonFiles: false, - isFlutter: isFlutter, - publishTo: 'none', - flutterConstraint: flutterConstraint, - dartConstraint: dartConstraint); - } else if (examples.isNotEmpty) { - final Directory examplesDirectory = getExampleDir(package)..createSync(); - for (final String exampleName in examples) { - createFakePackage(exampleName, examplesDirectory, - examples: [], - includeCommonFiles: false, - isFlutter: isFlutter, - publishTo: 'none', - flutterConstraint: flutterConstraint, - dartConstraint: dartConstraint); - } - } - - final p.Context posixContext = p.posix; - for (final String file in extraFiles) { - childFileWithSubcomponents(package.directory, posixContext.split(file)) - .createSync(recursive: true); - } - - return package; -} - -/// Creates a `pubspec.yaml` file for [package]. -/// -/// [platformSupport] is a map of platform string to the support details for -/// that platform. If empty, no `plugin` entry will be created unless `isPlugin` -/// is set to `true`. -void createFakePubspec( - RepositoryPackage package, { - String name = 'fake_package', - bool isFlutter = true, - bool isPlugin = false, - Map platformSupport = - const {}, - String? publishTo, - String? version, - String dartConstraint = _defaultDartConstraint, - String flutterConstraint = _defaultFlutterConstraint, -}) { - isPlugin |= platformSupport.isNotEmpty; - - String environmentSection = ''' -environment: - sdk: "$dartConstraint" -'''; - String dependenciesSection = ''' -dependencies: -'''; - String pluginSection = ''; - - // Add Flutter-specific entries if requested. - if (isFlutter) { - environmentSection += ''' - flutter: "$flutterConstraint" -'''; - dependenciesSection += ''' - flutter: - sdk: flutter -'''; - - if (isPlugin) { - pluginSection += ''' -flutter: - plugin: - platforms: -'''; - for (final MapEntry platform - in platformSupport.entries) { - pluginSection += - _pluginPlatformSection(platform.key, platform.value, name); - } - } - } - - // Default to a fake server to avoid ever accidentally publishing something - // from a test. Does not use 'none' since that changes the behavior of some - // commands. - final String publishToSection = - 'publish_to: ${publishTo ?? 'http://no_pub_server.com'}'; - - final String yaml = ''' -name: $name -${(version != null) ? 'version: $version' : ''} -$publishToSection - -$environmentSection - -$dependenciesSection - -$pluginSection -'''; - - package.pubspecFile.createSync(); - package.pubspecFile.writeAsStringSync(yaml); -} - -String _pluginPlatformSection( - String platform, PlatformDetails support, String packageName) { - String entry = ''; - // Build the main plugin entry. - if (support.type == PlatformSupport.federated) { - entry = ''' - $platform: - default_package: ${packageName}_$platform -'''; - } else { - final List lines = [ - ' $platform:', - ]; - switch (platform) { - case platformAndroid: - lines.add(' package: io.flutter.plugins.fake'); - continue nativeByDefault; - nativeByDefault: - case platformIOS: - case platformLinux: - case platformMacOS: - case platformWindows: - if (support.hasNativeCode) { - final String className = - platform == platformIOS ? 'FLTFakePlugin' : 'FakePlugin'; - lines.add(' pluginClass: $className'); - } - if (support.hasDartCode) { - lines.add(' dartPluginClass: FakeDartPlugin'); - } - break; - case platformWeb: - lines.addAll([ - ' pluginClass: FakePlugin', - ' fileName: ${packageName}_web.dart', - ]); - break; - default: - assert(false, 'Unrecognized platform: $platform'); - break; - } - entry = '${lines.join('\n')}\n'; - } - - return entry; -} - -/// Run the command [runner] with the given [args] and return -/// what was printed. -/// A custom [errorHandler] can be used to handle the runner error as desired without throwing. -Future> runCapturingPrint( - CommandRunner runner, - List args, { - Function(Error error)? errorHandler, - Function(Exception error)? exceptionHandler, -}) async { - final List prints = []; - final ZoneSpecification spec = ZoneSpecification( - print: (_, __, ___, String message) { - prints.add(message); - }, - ); - try { - await Zone.current - .fork(specification: spec) - .run>(() => runner.run(args)); - } on Error catch (e) { - if (errorHandler == null) { - rethrow; - } - errorHandler(e); - } on Exception catch (e) { - if (exceptionHandler == null) { - rethrow; - } - exceptionHandler(e); - } - - return prints; -} - -/// A mock [ProcessRunner] which records process calls. -class RecordingProcessRunner extends ProcessRunner { - final List recordedCalls = []; - - /// Maps an executable to a list of processes that should be used for each - /// successive call to it via [run], [runAndStream], or [start]. - final Map> mockProcessesForExecutable = - >{}; - - @override - Future runAndStream( - String executable, - List args, { - Directory? workingDir, - bool exitOnError = false, - }) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - final io.Process? processToReturn = _getProcessToReturn(executable); - final int exitCode = - processToReturn == null ? 0 : await processToReturn.exitCode; - if (exitOnError && (exitCode != 0)) { - throw io.ProcessException(executable, args); - } - return Future.value(exitCode); - } - - /// Returns [io.ProcessResult] created from [mockProcessesForExecutable]. - @override - Future run( - String executable, - List args, { - Directory? workingDir, - bool exitOnError = false, - bool logOnError = false, - Encoding stdoutEncoding = io.systemEncoding, - Encoding stderrEncoding = io.systemEncoding, - }) async { - recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); - - final io.Process? process = _getProcessToReturn(executable); - final List? processStdout = - await process?.stdout.transform(stdoutEncoding.decoder).toList(); - final String stdout = processStdout?.join() ?? ''; - final List? processStderr = - await process?.stderr.transform(stderrEncoding.decoder).toList(); - final String stderr = processStderr?.join() ?? ''; - - final io.ProcessResult result = process == null - ? io.ProcessResult(1, 0, '', '') - : io.ProcessResult(process.pid, await process.exitCode, stdout, stderr); - - if (exitOnError && (result.exitCode != 0)) { - throw io.ProcessException(executable, args); - } - - return Future.value(result); - } - - @override - Future start(String executable, List args, - {Directory? workingDirectory}) async { - recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); - return Future.value( - _getProcessToReturn(executable) ?? MockProcess()); - } - - io.Process? _getProcessToReturn(String executable) { - final List? processes = mockProcessesForExecutable[executable]; - if (processes != null && processes.isNotEmpty) { - return processes.removeAt(0); - } - return null; - } -} - -/// A recorded process call. -@immutable -class ProcessCall { - const ProcessCall(this.executable, this.args, this.workingDir); - - /// The executable that was called. - final String executable; - - /// The arguments passed to [executable] in the call. - final List args; - - /// The working directory this process was called from. - final String? workingDir; - - @override - bool operator ==(Object other) { - return other is ProcessCall && - executable == other.executable && - listsEqual(args, other.args) && - workingDir == other.workingDir; - } - - @override - int get hashCode => Object.hash(executable, args, workingDir); - - @override - String toString() { - final List command = [executable, ...args]; - return '"${command.join(' ')}" in $workingDir'; - } -} diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart deleted file mode 100644 index d485d81ceaf2..000000000000 --- a/script/tool/test/version_check_command_test.dart +++ /dev/null @@ -1,1468 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/version_check_command.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; -import 'package:mockito/mockito.dart'; -import 'package:pub_semver/pub_semver.dart'; -import 'package:test/test.dart'; - -import 'common/package_command_test.mocks.dart'; -import 'mocks.dart'; -import 'util.dart'; - -void testAllowedVersion( - String mainVersion, - String headVersion, { - bool allowed = true, - NextVersionType? nextVersionType, -}) { - final Version main = Version.parse(mainVersion); - final Version head = Version.parse(headVersion); - final Map allowedVersions = - getAllowedNextVersions(main, newVersion: head); - if (allowed) { - expect(allowedVersions, contains(head)); - if (nextVersionType != null) { - expect(allowedVersions[head], equals(nextVersionType)); - } - } else { - expect(allowedVersions, isNot(contains(head))); - } -} - -class MockProcessResult extends Mock implements io.ProcessResult {} - -void main() { - const String indentation = ' '; - group('VersionCheckCommand', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - late MockGitDir gitDir; - // Ignored if mockHttpResponse is set. - int mockHttpStatus; - Map? mockHttpResponse; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - - gitDir = MockGitDir(); - when(gitDir.path).thenReturn(packagesDir.parent.path); - when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) - .thenAnswer((Invocation invocation) { - final List arguments = - invocation.positionalArguments[0]! as List; - // Route git calls through the process runner, to make mock output - // consistent with other processes. Attach the first argument to the - // command to make targeting the mock results easier. - final String gitCommand = arguments.removeAt(0); - return processRunner.run('git-$gitCommand', arguments); - }); - - // Default to simulating the plugin never having been published. - mockHttpStatus = 404; - mockHttpResponse = null; - final MockClient mockClient = MockClient((http.Request request) async { - return http.Response(json.encode(mockHttpResponse), - mockHttpResponse == null ? mockHttpStatus : 200); - }); - - processRunner = RecordingProcessRunner(); - final VersionCheckCommand command = VersionCheckCommand(packagesDir, - processRunner: processRunner, - platform: mockPlatform, - gitDir: gitDir, - httpClient: mockClient); - - runner = CommandRunner( - 'version_check_command', 'Test for $VersionCheckCommand'); - runner.addCommand(command); - }); - - test('allows valid version', () async { - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.0.0 -> 2.0.0'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('denies invalid version', () async { - createFakePlugin('plugin', packagesDir, version: '0.2.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 0.0.1'), - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Incorrectly updated version.'), - ])); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('uses merge-base without explicit base-sha', () async { - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-merge-base'] = [ - MockProcess(stdout: 'abc123'), - MockProcess(stdout: 'abc123'), - ]; - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - final List output = - await runCapturingPrint(runner, ['version-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.0.0 -> 2.0.0'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-merge-base', - ['--fork-point', 'FETCH_HEAD', 'HEAD'], null), - ProcessCall('git-show', - ['abc123:packages/plugin/pubspec.yaml'], null), - ])); - }); - - test('allows valid version for new package.', () async { - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - final List output = - await runCapturingPrint(runner, ['version-check']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Unable to find previous version at git base.'), - ]), - ); - }); - - test('allows likely reverts.', () async { - createFakePlugin('plugin', packagesDir, version: '0.6.1'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 0.6.2'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - - expect( - output, - containsAllInOrder([ - contains('New version is lower than previous version. ' - 'This is assumed to be a revert.'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('denies lower version that could not be a simple revert', () async { - createFakePlugin('plugin', packagesDir, version: '0.5.1'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 0.6.2'), - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Incorrectly updated version.'), - ])); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('allows minor changes to platform interfaces', () async { - createFakePlugin('plugin_platform_interface', packagesDir, - version: '1.1.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.0.0 -> 1.1.0'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', - [ - 'main:packages/plugin_platform_interface/pubspec.yaml' - ], - null) - ])); - }); - - test('disallows breaking changes to platform interfaces by default', - () async { - createFakePlugin('plugin_platform_interface', packagesDir, - version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains( - ' Breaking changes to platform interfaces are not allowed ' - 'without explicit justification.\n' - ' See https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages ' - 'for more information.'), - ])); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', - [ - 'main:packages/plugin_platform_interface/pubspec.yaml' - ], - null) - ])); - }); - - test('allows breaking changes to platform interfaces with override label', - () async { - createFakePlugin('plugin_platform_interface', packagesDir, - version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=main', - '--pr-labels=some label,override: allow breaking change,another-label' - ]); - - expect( - output, - containsAllInOrder([ - contains('Allowing breaking change to plugin_platform_interface ' - 'due to the "override: allow breaking change" label.'), - contains('Ran for 1 package(s) (1 with warnings)'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', - [ - 'main:packages/plugin_platform_interface/pubspec.yaml' - ], - null) - ])); - }); - - test('allows breaking changes to platform interfaces with bypass flag', - () async { - createFakePlugin('plugin_platform_interface', packagesDir, - version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=main', - '--ignore-platform-interface-breaks' - ]); - - expect( - output, - containsAllInOrder([ - contains('Allowing breaking change to plugin_platform_interface due ' - 'to --ignore-platform-interface-breaks'), - contains('Ran for 1 package(s) (1 with warnings)'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall( - 'git-show', - [ - 'main:packages/plugin_platform_interface/pubspec.yaml' - ], - null) - ])); - }); - - test('Allow empty lines in front of the first version in CHANGELOG', - () async { - const String version = '1.0.1'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: version); - const String changelog = ''' - -## $version -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test('Throws if versions in changelog and pubspec do not match', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.1'); - const String changelog = ''' -## 1.0.2 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), - ]), - ); - }); - - test('Success if CHANGELOG and pubspec versions match', () async { - const String version = '1.0.1'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: version); - - const String changelog = ''' -## $version -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test( - 'Fail if pubspec version only matches an older version listed in CHANGELOG', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.1 -* Some changes. -## 1.0.0 -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - bool hasError = false; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - expect(e, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect( - output, - containsAllInOrder([ - contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), - ]), - ); - }); - - test('Allow NEXT as a placeholder for gathering CHANGELOG entries', - () async { - const String version = '1.0.0'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: version); - - const String changelog = ''' -## NEXT -* Some changes that won't be published until the next time there's a release. -## $version -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('Found NEXT; validating next version in the CHANGELOG.'), - ]), - ); - }); - - test('Fail if NEXT appears after a version', () async { - const String version = '1.0.1'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: version); - - const String changelog = ''' -## $version -* Some changes. -## NEXT -* Some changes that should have been folded in 1.0.1. -## 1.0.0 -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - bool hasError = false; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - expect(e, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect( - output, - containsAllInOrder([ - contains('When bumping the version for release, the NEXT section ' - "should be incorporated into the new version's release notes.") - ]), - ); - }); - - test('Fail if NEXT is left in the CHANGELOG when adding a version bump', - () async { - const String version = '1.0.1'; - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: version); - - const String changelog = ''' -## NEXT -* Some changes that should have been folded in 1.0.1. -## $version -* Some changes. -## 1.0.0 -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - - bool hasError = false; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - expect(e, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect( - output, - containsAllInOrder([ - contains('When bumping the version for release, the NEXT section ' - "should be incorporated into the new version's release notes."), - contains('plugin:\n' - ' CHANGELOG.md failed validation.'), - ]), - ); - }); - - test('fails if the version increases without replacing NEXT', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.1'); - - const String changelog = ''' -## NEXT -* Some changes that should be listed as part of 1.0.1. -## 1.0.0 -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - - bool hasError = false; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - expect(e, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect( - output, - containsAllInOrder([ - contains('When bumping the version for release, the NEXT section ' - "should be incorporated into the new version's release notes.") - ]), - ); - }); - - test('allows NEXT for a revert', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## NEXT -* Some changes that should be listed as part of 1.0.1. -## 1.0.0 -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.1'), - ]; - - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - expect( - output, - containsAllInOrder([ - contains('New version is lower than previous version. ' - 'This is assumed to be a revert.'), - ]), - ); - }); - - test( - 'fails gracefully if the version headers are not found due to using the wrong style', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## NEXT -* Some changes for a later release. -# 1.0.0 -* Some other changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=main', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Unable to find a version in CHANGELOG.md'), - contains('The current version should be on a line starting with ' - '"## ", either on the first non-empty line or after a "## NEXT" ' - 'section.'), - ]), - ); - }); - - test('fails gracefully if the version is unparseable', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## Alpha -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - - Error? commandError; - final List output = await runCapturingPrint(runner, [ - 'version-check', - '--base-sha=main', - ], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('"Alpha" could not be parsed as a version.'), - ]), - ); - }); - - group('missing change detection', () { - Future> runWithMissingChangeDetection(List extraArgs, - {void Function(Error error)? errorHandler}) async { - return runCapturingPrint( - runner, - [ - 'version-check', - '--base-sha=main', - '--check-for-missing-changes', - ...extraArgs, - ], - errorHandler: errorHandler); - } - - test('passes for unchanged packages', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test( - 'fails if a version change is missing from a change that does not ' - 'pass the exemption check', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/lib/plugin.dart -'''), - ]; - - Error? commandError; - final List output = await runWithMissingChangeDetection( - [], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No version change found'), - contains('plugin:\n' - ' Missing version change'), - ]), - ); - }); - - test('passes version change requirement when version changes', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.1'); - - const String changelog = ''' -## 1.0.1 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/lib/plugin.dart -packages/plugin/CHANGELOG.md -packages/plugin/pubspec.yaml -'''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test('version change check ignores files outside the package', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin_a/lib/plugin.dart -tool/plugin/lib/plugin.dart -'''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test('allows missing version change for exempt changes', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/example/android/lint-baseline.xml -packages/plugin/example/android/src/androidTest/foo/bar/FooTest.java -packages/plugin/example/ios/RunnerTests/Foo.m -packages/plugin/example/ios/RunnerUITests/info.plist -packages/plugin/CHANGELOG.md -'''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test('allows missing version change with override label', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/lib/plugin.dart -packages/plugin/CHANGELOG.md -packages/plugin/pubspec.yaml -'''), - ]; - - final List output = - await runWithMissingChangeDetection([ - '--pr-labels=some label,override: no versioning needed,another-label' - ]); - - expect( - output, - containsAllInOrder([ - contains('Ignoring lack of version change due to the ' - '"override: no versioning needed" label.'), - ]), - ); - }); - - test('fails if a CHANGELOG change is missing', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/example/lib/foo.dart -'''), - ]; - - Error? commandError; - final List output = await runWithMissingChangeDetection( - [], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No CHANGELOG change found'), - contains('plugin:\n' - ' Missing CHANGELOG change'), - ]), - ); - }); - - test('passes CHANGELOG check when the CHANGELOG is changed', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/example/lib/foo.dart -packages/plugin/CHANGELOG.md -'''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test('fails CHANGELOG check if only another package CHANGELOG chages', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/example/lib/foo.dart -packages/another_plugin/CHANGELOG.md -'''), - ]; - - Error? commandError; - final List output = await runWithMissingChangeDetection( - [], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('No CHANGELOG change found'), - ]), - ); - }); - - test('allows missing CHANGELOG change with justification', () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - MockProcess(stdout: ''' -packages/plugin/example/lib/foo.dart -'''), - ]; - - final List output = - await runWithMissingChangeDetection([ - '--pr-labels=some label,override: no changelog needed,another-label' - ]); - - expect( - output, - containsAllInOrder([ - contains('Ignoring lack of CHANGELOG update due to the ' - '"override: no changelog needed" label.'), - ]), - ); - }); - - // This test ensures that Dependabot Gradle changes to test-only files - // aren't flagged by the version check. - test( - 'allows missing CHANGELOG and version change for test-only Gradle changes', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - // File list. - MockProcess(stdout: ''' -packages/plugin/android/build.gradle -'''), - // build.gradle diff - MockProcess(stdout: ''' -- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -- testImplementation 'junit:junit:4.10.0' -+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' -+ testImplementation 'junit:junit:4.13.2' -'''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - - test('allows missing CHANGELOG and version change for dev-only changes', - () async { - final RepositoryPackage plugin = - createFakePlugin('plugin', packagesDir, version: '1.0.0'); - - const String changelog = ''' -## 1.0.0 -* Some changes. -'''; - plugin.changelogFile.writeAsStringSync(changelog); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - processRunner.mockProcessesForExecutable['git-diff'] = [ - // File list. - MockProcess(stdout: ''' -packages/plugin/tool/run_tests.dart -packages/plugin/run_tests.sh -'''), - ]; - - final List output = - await runWithMissingChangeDetection([]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - ]), - ); - }); - }); - - test('allows valid against pub', () async { - mockHttpResponse = { - 'name': 'some_package', - 'versions': [ - '0.0.1', - '0.0.2', - '1.0.0', - ], - }; - - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - final List output = await runCapturingPrint(runner, - ['version-check', '--base-sha=main', '--against-pub']); - - expect( - output, - containsAllInOrder([ - contains('plugin: Current largest version on pub: 1.0.0'), - ]), - ); - }); - - test('denies invalid against pub', () async { - mockHttpResponse = { - 'name': 'some_package', - 'versions': [ - '0.0.1', - '0.0.2', - ], - }; - - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - - bool hasError = false; - final List result = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - expect(e, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect( - result, - containsAllInOrder([ - contains(''' -${indentation}Incorrectly updated version. -${indentation}HEAD: 2.0.0, pub: 0.0.2. -${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: NextVersionType.MINOR, 0.0.3: NextVersionType.PATCH}''') - ]), - ); - }); - - test( - 'throw and print error message if http request failed when checking against pub', - () async { - mockHttpStatus = 400; - - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - bool hasError = false; - final List result = await runCapturingPrint( - runner, ['version-check', '--base-sha=main', '--against-pub'], - errorHandler: (Error e) { - expect(e, isA()); - hasError = true; - }); - expect(hasError, isTrue); - - expect( - result, - containsAllInOrder([ - contains(''' -${indentation}Error fetching version on pub for plugin. -${indentation}HTTP Status 400 -${indentation}HTTP response: null -''') - ]), - ); - }); - - test('when checking against pub, allow any version if http status is 404.', - () async { - mockHttpStatus = 404; - - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - final List result = await runCapturingPrint(runner, - ['version-check', '--base-sha=main', '--against-pub']); - - expect( - result, - containsAllInOrder([ - contains('Unable to find previous version on pub server.'), - ]), - ); - }); - - group('prelease versions', () { - test( - 'allow an otherwise-valid transition that also adds a pre-release component', - () async { - createFakePlugin('plugin', packagesDir, version: '2.0.0-dev'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.0.0'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.0.0 -> 2.0.0-dev'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('allow releasing a pre-release', () async { - createFakePlugin('plugin', packagesDir, version: '1.2.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.2.0-dev'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.2.0-dev -> 1.2.0'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - // Allow abandoning a pre-release version in favor of a different version - // change type. - test( - 'allow an otherwise-valid transition that also removes a pre-release component', - () async { - createFakePlugin('plugin', packagesDir, version: '2.0.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.2.0-dev'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.2.0-dev -> 2.0.0'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('allow changing only the pre-release version', () async { - createFakePlugin('plugin', packagesDir, version: '1.2.0-dev.2'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 1.2.0-dev.1'), - ]; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('1.2.0-dev.1 -> 1.2.0-dev.2'), - ]), - ); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('denies invalid version change that also adds a pre-release', - () async { - createFakePlugin('plugin', packagesDir, version: '0.2.0-dev'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 0.0.1'), - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Incorrectly updated version.'), - ])); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('denies invalid version change that also removes a pre-release', - () async { - createFakePlugin('plugin', packagesDir, version: '0.2.0'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 0.0.1-dev'), - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Incorrectly updated version.'), - ])); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - - test('denies invalid version change between pre-releases', () async { - createFakePlugin('plugin', packagesDir, version: '0.2.0-dev'); - processRunner.mockProcessesForExecutable['git-show'] = [ - MockProcess(stdout: 'version: 0.0.1-dev'), - ]; - Error? commandError; - final List output = await runCapturingPrint( - runner, ['version-check', '--base-sha=main'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('Incorrectly updated version.'), - ])); - expect( - processRunner.recordedCalls, - containsAllInOrder(const [ - ProcessCall('git-show', - ['main:packages/plugin/pubspec.yaml'], null) - ])); - }); - }); - }); - - group('Pre 1.0', () { - test('nextVersion allows patch version', () { - testAllowedVersion('0.12.0', '0.12.0+1', - nextVersionType: NextVersionType.PATCH); - testAllowedVersion('0.12.0+4', '0.12.0+5', - nextVersionType: NextVersionType.PATCH); - }); - - test('nextVersion does not allow jumping patch', () { - testAllowedVersion('0.12.0', '0.12.0+2', allowed: false); - testAllowedVersion('0.12.0+2', '0.12.0+4', allowed: false); - }); - - test('nextVersion does not allow going back', () { - testAllowedVersion('0.12.0', '0.11.0', allowed: false); - testAllowedVersion('0.12.0+2', '0.12.0+1', allowed: false); - testAllowedVersion('0.12.0+1', '0.12.0', allowed: false); - }); - - test('nextVersion allows minor version', () { - testAllowedVersion('0.12.0', '0.12.1', - nextVersionType: NextVersionType.MINOR); - testAllowedVersion('0.12.0+4', '0.12.1', - nextVersionType: NextVersionType.MINOR); - }); - - test('nextVersion does not allow jumping minor', () { - testAllowedVersion('0.12.0', '0.12.2', allowed: false); - testAllowedVersion('0.12.0+2', '0.12.3', allowed: false); - }); - }); - - group('Releasing 1.0', () { - test('nextVersion allows releasing 1.0', () { - testAllowedVersion('0.12.0', '1.0.0', - nextVersionType: NextVersionType.BREAKING_MAJOR); - testAllowedVersion('0.12.0+4', '1.0.0', - nextVersionType: NextVersionType.BREAKING_MAJOR); - }); - - test('nextVersion does not allow jumping major', () { - testAllowedVersion('0.12.0', '2.0.0', allowed: false); - testAllowedVersion('0.12.0+4', '2.0.0', allowed: false); - }); - - test('nextVersion does not allow un-releasing', () { - testAllowedVersion('1.0.0', '0.12.0+4', allowed: false); - testAllowedVersion('1.0.0', '0.12.0', allowed: false); - }); - }); - - group('Post 1.0', () { - test('nextVersion allows patch jumps', () { - testAllowedVersion('1.0.1', '1.0.2', - nextVersionType: NextVersionType.PATCH); - testAllowedVersion('1.0.0', '1.0.1', - nextVersionType: NextVersionType.PATCH); - }); - - test('nextVersion does not allow build jumps', () { - testAllowedVersion('1.0.1', '1.0.1+1', allowed: false); - testAllowedVersion('1.0.0+5', '1.0.0+6', allowed: false); - }); - - test('nextVersion does not allow skipping patches', () { - testAllowedVersion('1.0.1', '1.0.3', allowed: false); - testAllowedVersion('1.0.0', '1.0.6', allowed: false); - }); - - test('nextVersion allows minor version jumps', () { - testAllowedVersion('1.0.1', '1.1.0', - nextVersionType: NextVersionType.MINOR); - testAllowedVersion('1.0.0', '1.1.0', - nextVersionType: NextVersionType.MINOR); - }); - - test('nextVersion does not allow skipping minor versions', () { - testAllowedVersion('1.0.1', '1.2.0', allowed: false); - testAllowedVersion('1.1.0', '1.3.0', allowed: false); - }); - - test('nextVersion allows breaking changes', () { - testAllowedVersion('1.0.1', '2.0.0', - nextVersionType: NextVersionType.BREAKING_MAJOR); - testAllowedVersion('1.0.0', '2.0.0', - nextVersionType: NextVersionType.BREAKING_MAJOR); - }); - - test('nextVersion does not allow skipping major versions', () { - testAllowedVersion('1.0.1', '3.0.0', allowed: false); - testAllowedVersion('1.1.0', '2.3.0', allowed: false); - }); - }); -} diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart deleted file mode 100644 index 418c695f295c..000000000000 --- a/script/tool/test/xcode_analyze_command_test.dart +++ /dev/null @@ -1,484 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; - -import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:flutter_plugin_tools/src/common/core.dart'; -import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; -import 'package:flutter_plugin_tools/src/xcode_analyze_command.dart'; -import 'package:test/test.dart'; - -import 'mocks.dart'; -import 'util.dart'; - -// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of -// doing all the process mocking and validation. -void main() { - group('test xcode_analyze_command', () { - late FileSystem fileSystem; - late MockPlatform mockPlatform; - late Directory packagesDir; - late CommandRunner runner; - late RecordingProcessRunner processRunner; - - setUp(() { - fileSystem = MemoryFileSystem(); - mockPlatform = MockPlatform(isMacOS: true); - packagesDir = createPackagesDirectory(fileSystem: fileSystem); - processRunner = RecordingProcessRunner(); - final XcodeAnalyzeCommand command = XcodeAnalyzeCommand(packagesDir, - processRunner: processRunner, platform: mockPlatform); - - runner = CommandRunner( - 'xcode_analyze_command', 'Test for xcode_analyze_command'); - runner.addCommand(command); - }); - - test('Fails if no platforms are provided', () async { - Error? commandError; - final List output = await runCapturingPrint( - runner, ['xcode-analyze'], errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('At least one platform flag must be provided'), - ]), - ); - }); - - group('iOS', () { - test('skip if iOS is not supported', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final List output = - await runCapturingPrint(runner, ['xcode-analyze', '--ios']); - expect(output, - contains(contains('Not implemented for target platform(s).'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip if iOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.federated) - }); - - final List output = - await runCapturingPrint(runner, ['xcode-analyze', '--ios']); - expect(output, - contains(contains('Not implemented for target platform(s).'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('runs for iOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'xcode-analyze', - '--ios', - ]); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('plugin/example (iOS) passed analysis.') - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'generic/platform=iOS Simulator', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('passes min iOS deployment version when requested', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, - ['xcode-analyze', '--ios', '--ios-min-version=14.0']); - - expect( - output, - containsAllInOrder([ - contains('Running for plugin'), - contains('plugin/example (iOS) passed analysis.') - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'generic/platform=iOS Simulator', - 'IPHONEOS_DEPLOYMENT_TARGET=14.0', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, - [ - 'xcode-analyze', - '--ios', - ], - errorHandler: (Error e) { - commandError = e; - }, - ); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin'), - ])); - }); - }); - - group('macOS', () { - test('skip if macOS is not supported', () async { - createFakePlugin( - 'plugin', - packagesDir, - ); - - final List output = await runCapturingPrint( - runner, ['xcode-analyze', '--macos']); - expect(output, - contains(contains('Not implemented for target platform(s).'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('skip if macOS is implemented in a federated package', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.federated), - }); - - final List output = await runCapturingPrint( - runner, ['xcode-analyze', '--macos']); - expect(output, - contains(contains('Not implemented for target platform(s).'))); - expect(processRunner.recordedCalls, orderedEquals([])); - }); - - test('runs for macOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'xcode-analyze', - '--macos', - ]); - - expect(output, - contains(contains('plugin/example (macOS) passed analysis.'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('passes min macOS deployment version when requested', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, - ['xcode-analyze', '--macos', '--macos-min-version=12.0']); - - expect(output, - contains(contains('plugin/example (macOS) passed analysis.'))); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'MACOSX_DEPLOYMENT_TARGET=12.0', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('fails if xcrun fails', () async { - createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - processRunner.mockProcessesForExecutable['xcrun'] = [ - MockProcess(exitCode: 1) - ]; - - Error? commandError; - final List output = await runCapturingPrint( - runner, ['xcode-analyze', '--macos'], - errorHandler: (Error e) { - commandError = e; - }); - - expect(commandError, isA()); - expect( - output, - containsAllInOrder([ - contains('The following packages had errors:'), - contains(' plugin'), - ]), - ); - }); - }); - - group('combined', () { - test('runs both iOS and macOS when supported', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline), - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'xcode-analyze', - '--ios', - '--macos', - ]); - - expect( - output, - containsAll([ - contains('plugin/example (iOS) passed analysis.'), - contains('plugin/example (macOS) passed analysis.'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'generic/platform=iOS Simulator', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('runs only macOS for a macOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformMacOS: const PlatformDetails(PlatformSupport.inline), - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'xcode-analyze', - '--ios', - '--macos', - ]); - - expect( - output, - containsAllInOrder([ - contains('plugin/example (macOS) passed analysis.'), - ])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'macos/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('runs only iOS for a iOS plugin', () async { - final RepositoryPackage plugin = createFakePlugin('plugin', packagesDir, - platformSupport: { - platformIOS: const PlatformDetails(PlatformSupport.inline) - }); - - final Directory pluginExampleDirectory = getExampleDir(plugin); - - final List output = await runCapturingPrint(runner, [ - 'xcode-analyze', - '--ios', - '--macos', - ]); - - expect( - output, - containsAllInOrder( - [contains('plugin/example (iOS) passed analysis.')])); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall( - 'xcrun', - const [ - 'xcodebuild', - 'analyze', - '-workspace', - 'ios/Runner.xcworkspace', - '-scheme', - 'Runner', - '-configuration', - 'Debug', - '-destination', - 'generic/platform=iOS Simulator', - 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', - ], - pluginExampleDirectory.path), - ])); - }); - - test('skips when neither are supported', () async { - createFakePlugin('plugin', packagesDir); - - final List output = await runCapturingPrint(runner, [ - 'xcode-analyze', - '--ios', - '--macos', - ]); - - expect( - output, - containsAllInOrder([ - contains('SKIPPING: Not implemented for target platform(s).'), - ])); - - expect(processRunner.recordedCalls, orderedEquals([])); - }); - }); - }); -} diff --git a/script/tool_runner.sh b/script/tool_runner.sh index 221071550cc1..ba7bec6579d1 100755 --- a/script/tool_runner.sh +++ b/script/tool_runner.sh @@ -6,18 +6,20 @@ set -e # WARNING! Do not remove this script, or change its behavior, unless you have -# verified that it will not break the flutter/flutter analysis run of this -# repository: https://github.com/flutter/flutter/blob/master/dev/bots/test.dart +# verified that it will not break the dart-lang analysis run of this +# repository: https://github.com/dart-lang/sdk/blob/main/tools/bots/flutter/analyze_flutter_plugins.sh readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" -readonly TOOL_PATH="$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" -# Ensure that the tool dependencies have been fetched. -(pushd "$REPO_DIR/script/tool" && dart pub get && popd) >/dev/null # The tool expects to be run from the repo root. -cd "$REPO_DIR" -# Run from the in-tree source. # PACKAGE_SHARDING is (optionally) set from Cirrus. See .cirrus.yml -dart run "$TOOL_PATH" "$@" --packages-for-branch --log-timing $PACKAGE_SHARDING +cd "$REPO_DIR" +# Ensure that the tooling has been activated. +.ci/scripts/prepare_tool.sh + +dart pub global run flutter_plugin_tools "$@" \ + --packages-for-branch \ + --log-timing \ + $PACKAGE_SHARDING